use super::*;
#[test]
fn parse_envelope_slash_command() {
let raw = r#"{
"type": "slash_commands",
"envelope_id": "env-1",
"payload": {
"command": "/fleet",
"text": "",
"channel_id": "C123"
}
}"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::SlashCommand {
envelope_id: "env-1".into(),
command: "/fleet".into(),
text: String::new(),
channel: "C123".into(),
}
);
}
#[test]
fn parse_envelope_slash_command_with_text() {
let raw = r#"{
"type": "slash_commands",
"envelope_id": "env-2",
"payload": { "command": "/msend", "text": "sess-1 build now", "channel_id": "C9" }
}"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::SlashCommand {
envelope_id: "env-2".into(),
command: "/msend".into(),
text: "sess-1 build now".into(),
channel: "C9".into(),
}
);
}
#[test]
fn parse_envelope_message() {
let raw = r#"{
"type": "events_api",
"envelope_id": "env-3",
"payload": {
"event": { "type": "message", "text": "spin up a session", "channel": "C7", "user": "U1" }
}
}"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Message {
envelope_id: "env-3".into(),
text: "spin up a session".into(),
channel: "C7".into(),
}
);
}
#[test]
fn parse_envelope_ignores_bot_message() {
let raw = r#"{
"type": "events_api",
"envelope_id": "env-4",
"payload": {
"event": { "type": "message", "text": "hi", "channel": "C7", "bot_id": "B1" }
}
}"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Ignored {
envelope_id: Some("env-4".into())
}
);
}
#[test]
fn parse_envelope_ignores_subtype_bot_message() {
let raw = r#"{
"type": "events_api",
"envelope_id": "env-5",
"payload": {
"event": { "type": "message", "subtype": "bot_message", "text": "hi", "channel": "C7" }
}
}"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Ignored {
envelope_id: Some("env-5".into())
}
);
}
#[test]
fn parse_envelope_ignores_empty_message() {
let raw = r#"{
"type": "events_api",
"envelope_id": "env-6",
"payload": { "event": { "type": "message", "text": " ", "channel": "C7" } }
}"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Ignored {
envelope_id: Some("env-6".into())
}
);
}
#[test]
fn parse_envelope_hello_is_ignored() {
let raw = r#"{ "type": "hello" }"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Ignored { envelope_id: None }
);
}
#[test]
fn parse_envelope_disconnect_surfaces_reason() {
let raw = r#"{ "type": "disconnect", "reason": "refresh_requested" }"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Disconnect {
reason: "refresh_requested".into()
}
);
let raw = r#"{ "type": "disconnect" }"#;
assert_eq!(
parse_envelope(raw),
SlackEvent::Disconnect {
reason: String::new()
}
);
}
#[test]
fn classify_disconnect_reason_permanent_vs_transient() {
for reason in [
"app_deactivated",
"invalid_auth",
"account_inactive",
"token_revoked",
"token_expired",
"not_authed",
"missing_scope",
"no_permission",
] {
assert_eq!(
classify_disconnect_reason(reason),
DisconnectKind::Permanent,
"expected `{reason}` to be permanent",
);
}
assert_eq!(
classify_disconnect_reason("apps.connections.open failed: INVALID_AUTH"),
DisconnectKind::Permanent
);
for reason in [
"refresh_requested",
"warning",
"link_disabled",
"",
"socket gone",
] {
assert_eq!(
classify_disconnect_reason(reason),
DisconnectKind::Transient,
"expected `{reason}` to be transient",
);
}
}
#[test]
fn next_backoff_doubles_and_caps() {
use std::time::Duration;
let cap = Duration::from_secs(60);
assert_eq!(
next_backoff(Duration::from_secs(2), cap),
Duration::from_secs(4)
);
assert_eq!(
next_backoff(Duration::from_secs(4), cap),
Duration::from_secs(8)
);
assert_eq!(next_backoff(Duration::from_secs(40), cap), cap);
assert_eq!(next_backoff(cap, cap), cap);
assert_eq!(next_backoff(Duration::MAX, cap), cap);
}
#[test]
fn parse_envelope_garbage_is_ignored() {
assert_eq!(
parse_envelope("not json"),
SlackEvent::Ignored { envelope_id: None }
);
}
#[test]
fn ack_frame_shape() {
assert_eq!(ack_frame("env-1"), r#"{"envelope_id":"env-1"}"#);
}
#[test]
fn envelope_id_of_extracts_each_variant() {
assert_eq!(
envelope_id_of(&SlackEvent::SlashCommand {
envelope_id: "a".into(),
command: "/x".into(),
text: String::new(),
channel: "C".into(),
}),
Some("a".to_string())
);
assert_eq!(
envelope_id_of(&SlackEvent::Message {
envelope_id: "b".into(),
text: "hi".into(),
channel: "C".into(),
}),
Some("b".to_string())
);
assert_eq!(
envelope_id_of(&SlackEvent::Ignored {
envelope_id: Some("c".into())
}),
Some("c".to_string())
);
assert_eq!(
envelope_id_of(&SlackEvent::Ignored { envelope_id: None }),
None
);
}
#[test]
fn record_chat_turn_caps_history() {
let mut entry: Vec<ChatMessage> = Vec::new();
let turns = MAX_CHAT_HISTORY_TURNS;
let messages_pushed = 2 * turns;
for i in 0..turns {
record_chat_turn(&mut entry, &format!("u{i}"), &format!("a{i}"));
}
assert!(
messages_pushed > MAX_CHAT_HISTORY_TURNS,
"sanity: we must push MORE than the cap so this test is not vacuous"
);
assert_eq!(entry.len(), MAX_CHAT_HISTORY_TURNS);
let first_retained_turn = MAX_CHAT_HISTORY_TURNS / 2;
assert_eq!(entry[0].role, "user");
assert_eq!(entry[0].content, format!("u{first_retained_turn}"));
assert!(
!entry
.iter()
.any(|m| m.content == format!("u{}", first_retained_turn - 1)),
"the turn before the first retained one must have been evicted"
);
let last_user = entry
.iter()
.rev()
.find_map(|m| (m.role == "user").then(|| m.content.clone()));
assert_eq!(
last_user.as_deref(),
Some(&format!("u{}", MAX_CHAT_HISTORY_TURNS - 1)[..])
);
}
#[test]
fn pid_file_guard_removes_on_drop() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("slack.pid");
std::fs::write(&path, "12345").expect("write pid");
assert!(path.exists(), "precondition: PID file exists before drop");
{
let _guard = PidFileGuard { path: path.clone() };
} assert!(!path.exists(), "guard must remove the PID file on drop");
}
#[test]
fn pid_file_guard_drop_missing_is_silent() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("slack.pid");
{
let _guard = PidFileGuard { path: path.clone() };
}
assert!(!path.exists(), "missing PID file stays absent, no panic");
}
#[test]
fn record_chat_turn_skips_empty_reply() {
let mut entry: Vec<ChatMessage> = Vec::new();
record_chat_turn(&mut entry, "hello", "");
assert_eq!(entry.len(), 1);
assert_eq!(entry[0].role, "user");
}
#[test]
fn action_footer_lists_verbs() {
let footer = action_footer(Some(&["sessions.list".into(), "sessions.health".into()]));
assert_eq!(
footer.as_deref(),
Some("\n\n_ran: sessions.list, sessions.health_")
);
}
#[test]
fn action_footer_absent_when_empty() {
assert_eq!(action_footer(None), None);
let empty: Vec<String> = Vec::new();
assert_eq!(action_footer(Some(&empty)), None);
}
#[test]
fn resolve_token_reads_dotenv() {
use std::io::Write;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join(".env.local");
let mut f = std::fs::File::create(&path).expect("create dotenv");
writeln!(f, "# a comment").unwrap();
writeln!(f, "SLACK_BOT_TOKEN=\"xoxb-secret\"").unwrap();
drop(f);
assert_eq!(
read_dotenv_key(&path, "SLACK_BOT_TOKEN").as_deref(),
Some("xoxb-secret")
);
assert_eq!(read_dotenv_key(&path, "MISSING"), None);
}
#[test]
fn resolve_token_missing_is_none() {
let missing = std::path::Path::new("/nonexistent/.env.local");
assert_eq!(read_dotenv_key(missing, "SLACK_BOT_TOKEN"), None);
}
#[test]
fn pid_file_path_is_under_framework_root() {
let path = pid_file_path();
assert!(path.ends_with("slack.pid"));
assert!(
path.to_string_lossy()
.contains(crate::core::paths::FRAMEWORK_DIR_NAME)
);
}
#[test]
fn stop_via_pid_file_missing_is_not_running() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("slack.pid");
assert_eq!(stop_via_pid_file(&path), StopOutcome::NotRunning);
}
#[test]
fn stop_via_pid_file_garbage_is_failed_and_removes_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("slack.pid");
std::fs::write(&path, "not-a-pid").expect("write pid");
match stop_via_pid_file(&path) {
StopOutcome::Failed(_) => {}
other => panic!("expected Failed, got {other:?}"),
}
assert!(!path.exists(), "corrupt PID file should be removed");
}
#[cfg(unix)]
#[test]
fn stop_via_pid_file_stale_pid_is_not_running() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("slack.pid");
std::fs::write(&path, "999999999").expect("write pid");
assert_eq!(stop_via_pid_file(&path), StopOutcome::NotRunning);
assert!(!path.exists(), "stale PID file should be removed");
}