fix(channels): use platform message IDs to prevent duplicate memories
Fixes #430 - Prevents duplicate memories after restart by using platform message IDs instead of random UUIDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3cc835346
commit
e8553a800a
9 changed files with 217 additions and 82 deletions
|
|
@ -343,11 +343,16 @@ impl Channel for DiscordChannel {
|
|||
continue;
|
||||
}
|
||||
|
||||
let message_id = d.get("id").and_then(|i| i.as_str()).unwrap_or("");
|
||||
let channel_id = d.get("channel_id").and_then(|c| c.as_str()).unwrap_or("").to_string();
|
||||
|
||||
let channel_msg = ChannelMessage {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
sender: channel_id,
|
||||
id: if message_id.is_empty() {
|
||||
Uuid::new_v4().to_string()
|
||||
} else {
|
||||
format!("discord_{message_id}")
|
||||
},
|
||||
sender: author_id.to_string(),
|
||||
content: content.to_string(),
|
||||
channel: "discord".to_string(),
|
||||
timestamp: std::time::SystemTime::now()
|
||||
|
|
@ -695,4 +700,55 @@ mod tests {
|
|||
let guard = ch.typing_handle.lock().unwrap();
|
||||
assert!(guard.is_some());
|
||||
}
|
||||
|
||||
// ── Message ID edge cases ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn discord_message_id_format_includes_discord_prefix() {
|
||||
// Verify that message IDs follow the format: discord_{message_id}
|
||||
let message_id = "123456789012345678";
|
||||
let expected_id = format!("discord_{message_id}");
|
||||
assert_eq!(expected_id, "discord_123456789012345678");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discord_message_id_is_deterministic() {
|
||||
// Same message_id = same ID (prevents duplicates after restart)
|
||||
let message_id = "123456789012345678";
|
||||
let id1 = format!("discord_{message_id}");
|
||||
let id2 = format!("discord_{message_id}");
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discord_message_id_different_message_different_id() {
|
||||
// Different message IDs produce different IDs
|
||||
let id1 = format!("discord_123456789012345678");
|
||||
let id2 = format!("discord_987654321098765432");
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discord_message_id_uses_snowflake_id() {
|
||||
// Discord snowflake IDs are numeric strings
|
||||
let message_id = "123456789012345678"; // Typical snowflake format
|
||||
let id = format!("discord_{message_id}");
|
||||
assert!(id.starts_with("discord_"));
|
||||
// Snowflake IDs are numeric
|
||||
assert!(message_id.chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discord_message_id_fallback_to_uuid_on_empty() {
|
||||
// Edge case: empty message_id falls back to UUID
|
||||
let message_id = "";
|
||||
let id = if message_id.is_empty() {
|
||||
format!("discord_{}", uuid::Uuid::new_v4())
|
||||
} else {
|
||||
format!("discord_{message_id}")
|
||||
};
|
||||
assert!(id.starts_with("discord_"));
|
||||
// Should have UUID dashes
|
||||
assert!(id.contains('-'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ impl Channel for SlackChannel {
|
|||
last_ts = ts.to_string();
|
||||
|
||||
let channel_msg = ChannelMessage {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
sender: channel_id.clone(),
|
||||
id: format!("slack_{channel_id}_{ts}"),
|
||||
sender: user.to_string(),
|
||||
content: text.to_string(),
|
||||
channel: "slack".to_string(),
|
||||
timestamp: std::time::SystemTime::now()
|
||||
|
|
@ -252,4 +252,53 @@ mod tests {
|
|||
assert!(ch.is_user_allowed("U111"));
|
||||
assert!(ch.is_user_allowed("anyone"));
|
||||
}
|
||||
|
||||
// ── Message ID edge cases ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn slack_message_id_format_includes_channel_and_ts() {
|
||||
// Verify that message IDs follow the format: slack_{channel_id}_{ts}
|
||||
let ts = "1234567890.123456";
|
||||
let channel_id = "C12345";
|
||||
let expected_id = format!("slack_{channel_id}_{ts}");
|
||||
assert_eq!(expected_id, "slack_C12345_1234567890.123456");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slack_message_id_is_deterministic() {
|
||||
// Same channel_id + same ts = same ID (prevents duplicates after restart)
|
||||
let ts = "1234567890.123456";
|
||||
let channel_id = "C12345";
|
||||
let id1 = format!("slack_{channel_id}_{ts}");
|
||||
let id2 = format!("slack_{channel_id}_{ts}");
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slack_message_id_different_ts_different_id() {
|
||||
// Different timestamps produce different IDs
|
||||
let channel_id = "C12345";
|
||||
let id1 = format!("slack_{channel_id}_1234567890.123456");
|
||||
let id2 = format!("slack_{channel_id}_1234567890.123457");
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slack_message_id_different_channel_different_id() {
|
||||
// Different channels produce different IDs even with same ts
|
||||
let ts = "1234567890.123456";
|
||||
let id1 = format!("slack_C12345_{ts}");
|
||||
let id2 = format!("slack_C67890_{ts}");
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slack_message_id_no_uuid_randomness() {
|
||||
// Verify format doesn't contain random UUID components
|
||||
let ts = "1234567890.123456";
|
||||
let channel_id = "C12345";
|
||||
let id = format!("slack_{channel_id}_{ts}");
|
||||
assert!(!id.contains('-')); // No UUID dashes
|
||||
assert!(id.starts_with("slack_"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,6 +579,11 @@ Allowlist Telegram @username or numeric user ID, then run `zeroclaw onboard --ch
|
|||
continue;
|
||||
};
|
||||
|
||||
let message_id = message
|
||||
.get("message_id")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Send "typing" indicator immediately when we receive a message
|
||||
let typing_body = serde_json::json!({
|
||||
"chat_id": &chat_id,
|
||||
|
|
@ -592,8 +597,8 @@ Allowlist Telegram @username or numeric user ID, then run `zeroclaw onboard --ch
|
|||
.await; // Ignore errors for typing indicator
|
||||
|
||||
let msg = ChannelMessage {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
sender: chat_id,
|
||||
id: format!("telegram_{chat_id}_{message_id}"),
|
||||
sender: username.to_string(),
|
||||
content: text.to_string(),
|
||||
channel: "telegram".to_string(),
|
||||
timestamp: std::time::SystemTime::now()
|
||||
|
|
@ -1033,4 +1038,62 @@ mod tests {
|
|||
// Should not panic
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ── Message ID edge cases ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn telegram_message_id_format_includes_chat_and_message_id() {
|
||||
// Verify that message IDs follow the format: telegram_{chat_id}_{message_id}
|
||||
let chat_id = "123456";
|
||||
let message_id = 789;
|
||||
let expected_id = format!("telegram_{chat_id}_{message_id}");
|
||||
assert_eq!(expected_id, "telegram_123456_789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telegram_message_id_is_deterministic() {
|
||||
// Same chat_id + same message_id = same ID (prevents duplicates after restart)
|
||||
let chat_id = "123456";
|
||||
let message_id = 789;
|
||||
let id1 = format!("telegram_{chat_id}_{message_id}");
|
||||
let id2 = format!("telegram_{chat_id}_{message_id}");
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telegram_message_id_different_message_different_id() {
|
||||
// Different message IDs produce different IDs
|
||||
let chat_id = "123456";
|
||||
let id1 = format!("telegram_{chat_id}_789");
|
||||
let id2 = format!("telegram_{chat_id}_790");
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telegram_message_id_different_chat_different_id() {
|
||||
// Different chats produce different IDs even with same message_id
|
||||
let message_id = 789;
|
||||
let id1 = format!("telegram_123456_{message_id}");
|
||||
let id2 = format!("telegram_789012_{message_id}");
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telegram_message_id_no_uuid_randomness() {
|
||||
// Verify format doesn't contain random UUID components
|
||||
let chat_id = "123456";
|
||||
let message_id = 789;
|
||||
let id = format!("telegram_{chat_id}_{message_id}");
|
||||
assert!(!id.contains('-')); // No UUID dashes
|
||||
assert!(id.starts_with("telegram_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telegram_message_id_handles_zero_message_id() {
|
||||
// Edge case: message_id can be 0 (fallback/missing case)
|
||||
let chat_id = "123456";
|
||||
let message_id = 0;
|
||||
let id = format!("telegram_{chat_id}_{message_id}");
|
||||
assert_eq!(id, "telegram_123456_0");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue