use std::collections::{BTreeSet, HashMap};
use std::hash::{BuildHasher, Hash};
use std::time::{Duration, Instant};
use super::router::{HubCommand, RoutingDecision};
pub fn merge_routing_with_quote(
base: RoutingDecision,
quoted: Option<QuoteOrigin>,
) -> RoutingDecision {
if matches!(&base, RoutingDecision::HubInternal(_)) {
return base;
}
match quoted {
Some(QuoteOrigin::Client {
vtoken,
session_name,
..
}) => RoutingDecision::ForwardTo {
vtoken,
session_override: session_name,
},
Some(QuoteOrigin::Hub { cmd }) => RoutingDecision::HubInternal(cmd),
None => base,
}
}
#[derive(Debug, Clone)]
pub enum QuoteOrigin {
Client {
vtoken: String,
name: String,
label: Option<String>,
session_name: Option<String>,
},
Hub { cmd: HubCommand },
}
#[derive(Debug, Clone)]
struct ContentEntry {
scope: String,
origin: QuoteOrigin,
created_ms: i64,
seq: u64,
deadline: Instant,
}
#[derive(Debug)]
pub struct QuoteRouteIndex {
by_content: HashMap<u64, Vec<ContentEntry>>,
next_seq: u64,
hasher: std::collections::hash_map::RandomState,
by_age: BTreeSet<(i64, u64, u64)>,
}
impl Default for QuoteRouteIndex {
fn default() -> Self {
Self {
by_content: HashMap::new(),
next_seq: 0,
hasher: std::collections::hash_map::RandomState::new(),
by_age: BTreeSet::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct WarmItem {
pub scope: String,
pub text: String,
pub origin: QuoteOrigin,
}
const INDEX_TTL: Duration = Duration::from_secs(86400 * 7);
const MAX_CONTENT_ENTRIES_PER_KEY: usize = 32;
const CONTENT_PREFIX_CHARS: usize = 48;
const MAX_BY_CONTENT_KEYS: usize = 10_000;
pub const DEFAULT_QUOTE_INDEX_WARMUP_LIMIT: i64 = 500;
impl QuoteRouteIndex {
fn hash_key<T: Hash>(&self, t: &T) -> u64 {
self.hasher.hash_one(t)
}
pub fn register_outbound_content(&mut self, scope: &str, text: &str, origin: QuoteOrigin) {
let now_ms = now_millis();
let deadline = Instant::now() + INDEX_TTL;
self.evict_expired();
for key_str in content_keys(text) {
let key = self.hash_key(&key_str);
if !self.by_content.contains_key(&key) && self.by_content.len() >= MAX_BY_CONTENT_KEYS {
if let Some(&(oldest_ms, oldest_seq, oldest_key)) = self.by_age.iter().next() {
self.by_content.remove(&oldest_key);
self.by_age.remove(&(oldest_ms, oldest_seq, oldest_key));
}
}
let bucket = self.by_content.entry(key).or_default();
let seq = self.next_seq;
bucket.push(ContentEntry {
scope: scope.to_string(),
origin: origin.clone(),
created_ms: now_ms,
seq,
deadline,
});
self.by_age.insert((now_ms, seq, key));
self.next_seq = self.next_seq.wrapping_add(1);
if bucket.len() > MAX_CONTENT_ENTRIES_PER_KEY {
let overflow = bucket.len() - MAX_CONTENT_ENTRIES_PER_KEY;
for entry in bucket.drain(0..overflow) {
self.by_age.remove(&(entry.created_ms, entry.seq, key));
}
}
}
}
pub fn warm_from_history(&mut self, items: &[WarmItem]) -> usize {
let mut indexed = 0;
for item in items {
if item.text.trim().is_empty() {
continue;
}
self.register_outbound_content(&item.scope, &item.text, item.origin.clone());
indexed += 1;
}
indexed
}
fn resolve_by_content(
&self,
scope: &str,
text: &str,
ref_ms: Option<i64>,
) -> Option<QuoteOrigin> {
let now = Instant::now();
for key_str in content_keys(text) {
let key = self.hash_key(&key_str);
let Some(bucket) = self.by_content.get(&key) else {
continue;
};
let best = bucket
.iter()
.filter(|e| now <= e.deadline && e.scope == scope)
.min_by_key(|e| match ref_ms {
Some(ms) => (e.created_ms as i128 - ms as i128).abs(),
None => -(e.created_ms as i128),
});
if let Some(entry) = best {
return Some(entry.origin.clone());
}
}
None
}
pub fn resolve_user_quote(
&mut self,
scope: &str,
msg: &crate::ilink::types::WeixinMessage,
) -> Option<QuoteOrigin> {
let (text, ref_ms) = collect_quoted_content(msg)?;
self.resolve_by_content(scope, &text, ref_ms)
}
pub fn collect_quoted(
msg: &crate::ilink::types::WeixinMessage,
) -> Option<(String, Option<i64>)> {
collect_quoted_content(msg)
}
pub fn footer_from_user_quote(
msg: &crate::ilink::types::WeixinMessage,
) -> Option<(String, Option<String>)> {
let (text, _) = collect_quoted_content(msg)?;
parse_footer_from_quoted_text(&text)
}
pub fn evict_expired(&mut self) {
let now = Instant::now();
let mut dead_age_tuples: Vec<(i64, u64, u64)> = Vec::new();
for (key, bucket) in self.by_content.iter_mut() {
for entry in bucket.iter() {
if entry.deadline <= now {
dead_age_tuples.push((entry.created_ms, entry.seq, *key));
}
}
bucket.retain(|e| now <= e.deadline);
}
self.by_content.retain(|_, bucket| !bucket.is_empty());
for t in dead_age_tuples {
self.by_age.remove(&t);
}
let orphans: Vec<(i64, u64, u64)> = self
.by_age
.iter()
.take_while(|t| !self.by_content.contains_key(&t.2))
.copied()
.collect();
for t in orphans {
self.by_age.remove(&t);
}
}
}
fn now_millis() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
fn content_sig(text: &str) -> String {
text.trim().to_string()
}
fn content_keys(text: &str) -> Vec<String> {
let sig = content_sig(text);
if sig.is_empty() {
return Vec::new();
}
let mut keys = vec![format!("full:{sig}")];
let prefix: String = sig.chars().take(CONTENT_PREFIX_CHARS).collect();
if prefix.len() < sig.len() {
keys.push(format!("pre:{prefix}"));
}
keys
}
pub fn parse_footer_from_quoted_text(text: &str) -> Option<(String, Option<String>)> {
let footer_line = if let Some(pos) = text.rfind("\n---\n") {
text[pos + 5..].trim()
} else if let Some(pos) = text.rfind("\n— ") {
text[pos + 4..].trim()
} else if let Some(stripped) = text.trim().strip_prefix("— ") {
stripped.trim()
} else {
return None;
};
if footer_line.is_empty() {
return None;
}
let parts: Vec<&str> = footer_line.split(" · ").collect();
let name = parts[0].trim();
if name.is_empty() {
return None;
}
let session = parts.iter().rev().find_map(|p| {
let p = p.trim();
if p.starts_with("at-") || p.starts_with("session-") {
Some(p.to_string())
} else {
None
}
});
Some((name.to_string(), session))
}
pub fn warm_item_from_recent_row(row: &crate::store::RecentOutboundRow) -> Option<WarmItem> {
let origin = match (
row.vtoken.as_deref(),
parse_footer_from_quoted_text(&row.text),
) {
(Some(vt), Some((name, session_name))) => QuoteOrigin::Client {
vtoken: vt.to_string(),
name,
label: None,
session_name,
},
(Some(vt), None) => {
QuoteOrigin::Client {
vtoken: vt.to_string(),
name: "<warmup>".to_string(),
label: None,
session_name: Some(row.session_name.clone()),
}
}
(None, _) => {
return None;
}
};
Some(WarmItem {
scope: row.from_user.clone(),
text: row.text.clone(),
origin,
})
}
fn collect_quoted_content(
msg: &crate::ilink::types::WeixinMessage,
) -> Option<(String, Option<i64>)> {
let items = msg.item_list.as_ref()?;
for item in items.iter() {
let Some(extra) = item.extra.as_object() else {
continue;
};
let Some(mi) = extra.get("ref_msg").and_then(|r| r.get("message_item")) else {
continue;
};
let text = mi
.get("text_item")
.and_then(|t| t.get("text"))
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty());
if let Some(text) = text {
let ref_ms = mi.get("create_time_ms").and_then(|v| v.as_i64());
return Some((text.to_string(), ref_ms));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ilink::types::{MessageItem, TextItem, WeixinMessage};
const SCOPE: &str = "user@x";
fn quote_reply(scope_text: &str, quoted_text: &str, ref_ms: Option<i64>) -> WeixinMessage {
let mut ref_item = serde_json::json!({
"ref_msg": {
"message_item": {
"type": 1,
"text_item": { "text": quoted_text }
}
}
});
if let Some(ms) = ref_ms {
ref_item["ref_msg"]["message_item"]["create_time_ms"] = serde_json::Value::from(ms);
}
WeixinMessage {
message_type: Some(1),
from_user_id: Some(SCOPE.into()),
item_list: Some(std::sync::Arc::new(vec![MessageItem {
item_type: Some(1),
text_item: Some(TextItem {
text: Some(scope_text.into()),
}),
extra: ref_item,
..Default::default()
}])),
..Default::default()
}
}
#[test]
fn merge_quote_overrides_forward() {
let base = RoutingDecision::ForwardTo {
vtoken: "default_vt".into(),
session_override: None,
};
let q = QuoteOrigin::Client {
vtoken: "quoted_vt".into(),
name: "n".into(),
label: None,
session_name: Some("feature-a".into()),
};
let out = merge_routing_with_quote(base, Some(q));
assert!(matches!(
out,
RoutingDecision::ForwardTo { ref vtoken, ref session_override }
if vtoken == "quoted_vt" && session_override.as_deref() == Some("feature-a")
));
}
#[test]
fn merge_quote_overrides_broadcast() {
let out = merge_routing_with_quote(
RoutingDecision::Broadcast,
Some(QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
}),
);
assert!(matches!(out, RoutingDecision::ForwardTo { ref vtoken, .. } if vtoken == "vt"));
}
#[test]
fn merge_hub_internal_from_quote() {
let out = merge_routing_with_quote(
RoutingDecision::ForwardTo {
vtoken: "x".into(),
session_override: None,
},
Some(QuoteOrigin::Hub {
cmd: HubCommand::List,
}),
);
assert!(matches!(
out,
RoutingDecision::HubInternal(HubCommand::List)
));
}
#[test]
fn merge_explicit_hub_command_not_overridden_by_quote() {
let base = RoutingDecision::HubInternal(HubCommand::Status);
let out = merge_routing_with_quote(
base,
Some(QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
}),
);
assert!(matches!(
out,
RoutingDecision::HubInternal(HubCommand::Status)
));
}
#[test]
fn merge_no_quote_keeps_forward() {
let base = RoutingDecision::ForwardTo {
vtoken: "keep".into(),
session_override: None,
};
let out = merge_routing_with_quote(base, None);
assert!(matches!(out, RoutingDecision::ForwardTo { ref vtoken, .. } if vtoken == "keep"));
}
#[test]
fn resolve_without_ref_returns_none() {
let mut idx = QuoteRouteIndex::default();
idx.register_outbound_content(
SCOPE,
"hi",
QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
},
);
let user = WeixinMessage {
item_list: Some(std::sync::Arc::new(vec![MessageItem {
item_type: Some(1),
text_item: Some(TextItem {
text: Some("hi".into()),
}),
..Default::default()
}])),
..Default::default()
};
assert!(idx.resolve_user_quote(SCOPE, &user).is_none());
}
#[test]
fn resolve_by_content_when_ref_has_no_msg_id() {
let mut idx = QuoteRouteIndex::default();
let sent = "你好!有什么我可以帮你的吗?\n\n— ilink-claude · session-20260611-125634";
idx.register_outbound_content(
SCOPE,
sent,
QuoteOrigin::Client {
vtoken: "vhub_867".into(),
name: "ilink-claude".into(),
label: None,
session_name: Some("session-20260611-125634".into()),
},
);
let user = quote_reply("你有什么工具", sent, Some(1781153810000));
match idx
.resolve_user_quote(SCOPE, &user)
.expect("content resolve")
{
QuoteOrigin::Client {
vtoken,
session_name,
..
} => {
assert_eq!(vtoken, "vhub_867");
assert_eq!(session_name.as_deref(), Some("session-20260611-125634"));
}
QuoteOrigin::Hub { .. } => panic!("expected client origin"),
}
}
#[test]
fn resolve_by_content_uses_timestamp_to_disambiguate() {
let mut idx = QuoteRouteIndex::default();
let text = "完成了";
idx.by_content.insert(
idx.hash_key(&format!("full:{text}")),
vec![
ContentEntry {
scope: SCOPE.into(),
origin: QuoteOrigin::Client {
vtoken: "vt_old".into(),
name: "a".into(),
label: None,
session_name: Some("s-old".into()),
},
created_ms: 1_000_000_000_000,
seq: 0,
deadline: Instant::now() + INDEX_TTL,
},
ContentEntry {
scope: SCOPE.into(),
origin: QuoteOrigin::Client {
vtoken: "vt_new".into(),
name: "b".into(),
label: None,
session_name: Some("s-new".into()),
},
created_ms: 1_000_000_050_000,
seq: 1,
deadline: Instant::now() + INDEX_TTL,
},
],
);
let origin = idx
.resolve_by_content(SCOPE, text, Some(1_000_000_001_000))
.expect("resolve");
match origin {
QuoteOrigin::Client { vtoken, .. } => assert_eq!(vtoken, "vt_old"),
_ => panic!("expected client"),
}
}
#[test]
fn resolve_is_scoped_per_conversation() {
let mut idx = QuoteRouteIndex::default();
let sent = "你好\n\n— ilink-claude";
idx.register_outbound_content(
"userA@x",
sent,
QuoteOrigin::Client {
vtoken: "vt_for_A".into(),
name: "ilink-claude".into(),
label: None,
session_name: Some("default".into()),
},
);
let user_b = quote_reply("再来", sent, Some(1781153810000));
assert!(idx.resolve_user_quote("userB@x", &user_b).is_none());
let user_a = quote_reply("再来", sent, Some(1781153810000));
assert!(idx.resolve_user_quote("userA@x", &user_a).is_some());
}
#[test]
fn resolve_hub_origin_from_content() {
let mut idx = QuoteRouteIndex::default();
let sent = "iLink Hub 帮助\n...";
idx.register_outbound_content(
SCOPE,
sent,
QuoteOrigin::Hub {
cmd: HubCommand::Help,
},
);
let user = quote_reply("再说一遍", sent, None);
assert!(matches!(
idx.resolve_user_quote(SCOPE, &user),
Some(QuoteOrigin::Hub {
cmd: HubCommand::Help
})
));
}
#[test]
fn register_outbound_content_respects_limit() {
let mut idx = QuoteRouteIndex::default();
for i in 0..10000 {
idx.register_outbound_content(
SCOPE,
&format!("msg_{}", i),
QuoteOrigin::Client {
vtoken: format!("vt_{}", i),
name: "n".into(),
label: None,
session_name: None,
},
);
}
assert_eq!(idx.by_content.len(), 10000);
idx.register_outbound_content(
SCOPE,
"msg_overflow",
QuoteOrigin::Client {
vtoken: "vt_overflow".into(),
name: "n".into(),
label: None,
session_name: None,
},
);
assert_eq!(idx.by_content.len(), 10000);
let user = quote_reply("reply", "msg_overflow", None);
let origin = idx
.resolve_user_quote(SCOPE, &user)
.expect("resolve overflow");
match origin {
QuoteOrigin::Client { vtoken, .. } => assert_eq!(vtoken, "vt_overflow"),
_ => panic!("expected client"),
}
let user_evicted = quote_reply("reply", "msg_0", None);
assert!(idx.resolve_user_quote(SCOPE, &user_evicted).is_none());
idx.register_outbound_content(
SCOPE,
"msg_1",
QuoteOrigin::Client {
vtoken: "vt_updated".into(),
name: "n".into(),
label: None,
session_name: None,
},
);
assert_eq!(idx.by_content.len(), 10000);
let user_updated = quote_reply("reply", "msg_1", None);
let origin = idx
.resolve_user_quote(SCOPE, &user_updated)
.expect("resolve");
match origin {
QuoteOrigin::Client { vtoken, .. } => assert_eq!(vtoken, "vt_updated"),
_ => panic!("expected client"),
}
}
#[test]
fn resolve_by_content_overflow_protection_min() {
let mut idx = QuoteRouteIndex::default();
let text = "overflow_min";
idx.register_outbound_content(
SCOPE,
text,
QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
},
);
let user = quote_reply("reply", text, Some(i64::MIN));
let origin = idx.resolve_user_quote(SCOPE, &user);
assert!(origin.is_some());
}
#[test]
fn resolve_by_content_overflow_protection_max() {
let mut idx = QuoteRouteIndex::default();
let text = "overflow_max";
idx.register_outbound_content(
SCOPE,
text,
QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
},
);
let user = quote_reply("reply", text, Some(i64::MAX));
let origin = idx.resolve_user_quote(SCOPE, &user);
assert!(origin.is_some());
}
#[test]
fn parse_footer_new_format_name_and_session() {
let text = "你好!有什么我可以帮你的吗?\n\n---\nilink-claude · session-20260611-125634";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("session-20260611-125634"));
}
#[test]
fn parse_footer_new_format_with_label() {
let text = "body\n\n---\nilink-claude · office · session-20260611-194813";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("session-20260611-194813"));
}
#[test]
fn parse_footer_old_format_em_dash() {
let text = "你好!有什么我可以帮你的吗?\n\n— ilink-claude · session-20260611-125634";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("session-20260611-125634"));
}
#[test]
fn parse_footer_at_mention_session() {
let text = "完成了\n\n---\nilink-claude · at-20260615-114019020";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert_eq!(session.as_deref(), Some("at-20260615-114019020"));
}
#[test]
fn parse_footer_name_only_no_session() {
let text = "hello\n\n---\nilink-claude";
let (name, session) = parse_footer_from_quoted_text(text).unwrap();
assert_eq!(name, "ilink-claude");
assert!(session.is_none());
}
#[test]
fn parse_footer_no_footer_returns_none() {
assert!(parse_footer_from_quoted_text("plain message without footer").is_none());
}
#[test]
fn at_mention_reply_quote_routes_back_to_at_session() {
let mut idx = QuoteRouteIndex::default();
let at_session = "at-20260624-092041904";
let reply_text = format!(
"只有 codebuddy 那个文件里提到 GLM...\n\n---\nilink-claude · KONGJIE-MC3 · {at_session}"
);
let vtoken_claude = "vt-2f43aec8";
idx.register_outbound_content(
"peer:user@wx",
&reply_text,
QuoteOrigin::Client {
vtoken: vtoken_claude.to_string(),
name: "ilink-claude".to_string(),
label: Some("KONGJIE-MC3".to_string()),
session_name: Some(at_session.to_string()),
},
);
let inbound = quote_reply("followup question", &reply_text, Some(1750000000000));
let origin = idx
.resolve_user_quote("peer:user@wx", &inbound)
.expect("quote_index must resolve the at-mention reply");
match origin {
QuoteOrigin::Client {
vtoken,
session_name,
..
} => {
assert_eq!(
vtoken, vtoken_claude,
"must route back to ilink-claude, not the home/default backend"
);
assert_eq!(
session_name.as_deref(),
Some(at_session),
"must resume the at-session, not fall back to the old active session"
);
}
QuoteOrigin::Hub { .. } => panic!("expected Client origin, got Hub"),
}
}
#[test]
fn at_mention_reply_not_confused_with_older_active_session() {
let mut idx = QuoteRouteIndex::default();
let scope = "peer:user@wx";
let vtoken = "vt-2f43aec8";
let old_reply = "目前这次对话里完成的事情...\n\n---\nilink-claude · KONGJIE-MC3 · session-20260623-181249";
idx.register_outbound_content(
scope,
old_reply,
QuoteOrigin::Client {
vtoken: vtoken.to_string(),
name: "ilink-claude".to_string(),
label: Some("KONGJIE-MC3".to_string()),
session_name: Some("session-20260623-181249".to_string()),
},
);
let at_reply =
"只有 codebuddy 那个文件里提到 GLM\n\n---\nilink-claude · KONGJIE-MC3 · at-20260624-092041904";
idx.register_outbound_content(
scope,
at_reply,
QuoteOrigin::Client {
vtoken: vtoken.to_string(),
name: "ilink-claude".to_string(),
label: Some("KONGJIE-MC3".to_string()),
session_name: Some("at-20260624-092041904".to_string()),
},
);
let q_at = quote_reply("follow up on glm", at_reply, Some(1750000100000));
let origin_at = idx
.resolve_user_quote(scope, &q_at)
.expect("must resolve at-session reply");
match origin_at {
QuoteOrigin::Client { session_name, .. } => {
assert_eq!(session_name.as_deref(), Some("at-20260624-092041904"));
}
_ => panic!("expected Client"),
}
let q_old = quote_reply("continue old session", old_reply, Some(1749900000000));
let origin_old = idx
.resolve_user_quote(scope, &q_old)
.expect("must resolve old session reply");
match origin_old {
QuoteOrigin::Client { session_name, .. } => {
assert_eq!(session_name.as_deref(), Some("session-20260623-181249"));
}
_ => panic!("expected Client"),
}
}
#[test]
fn warm_from_history_equivalent_to_register_loop() {
let items = vec![
WarmItem {
scope: SCOPE.into(),
text: "alpha\n\n---\nilink-claude · session-20260611-1".into(),
origin: QuoteOrigin::Client {
vtoken: "vt_a".into(),
name: "ilink-claude".into(),
label: None,
session_name: Some("session-20260611-1".into()),
},
},
WarmItem {
scope: SCOPE.into(),
text: "beta\n\n---\nilink-claude · session-20260611-2".into(),
origin: QuoteOrigin::Client {
vtoken: "vt_b".into(),
name: "ilink-claude".into(),
label: None,
session_name: Some("session-20260611-2".into()),
},
},
WarmItem {
scope: SCOPE.into(),
text: "list\n\n---\nhub".into(),
origin: QuoteOrigin::Hub {
cmd: HubCommand::List,
},
},
];
let mut warm_idx = QuoteRouteIndex::default();
let n = warm_idx.warm_from_history(&items);
assert_eq!(n, 3);
let mut live_idx = QuoteRouteIndex::default();
for item in &items {
live_idx.register_outbound_content(&item.scope, &item.text, item.origin.clone());
}
for item in &items {
let warm_origin = warm_idx
.resolve_by_content(SCOPE, &item.text, None)
.expect("warm resolve");
let live_origin = live_idx
.resolve_by_content(SCOPE, &item.text, None)
.expect("live resolve");
assert_eq!(
format!("{:?}", warm_origin),
format!("{:?}", live_origin),
"warm and live paths must agree for `{}`",
item.text
);
}
}
#[test]
fn warm_from_history_empty_slice_is_noop() {
let mut idx = QuoteRouteIndex::default();
assert_eq!(idx.warm_from_history(&[]), 0);
assert_eq!(idx.by_content.len(), 0);
}
#[test]
fn warm_from_history_skips_empty_text() {
let mut idx = QuoteRouteIndex::default();
let items = vec![
WarmItem {
scope: SCOPE.into(),
text: "".into(),
origin: QuoteOrigin::Client {
vtoken: "vt".into(),
name: "n".into(),
label: None,
session_name: None,
},
},
WarmItem {
scope: SCOPE.into(),
text: " ".into(), origin: QuoteOrigin::Client {
vtoken: "vt2".into(),
name: "n".into(),
label: None,
session_name: None,
},
},
];
assert_eq!(idx.warm_from_history(&items), 0);
assert_eq!(idx.by_content.len(), 0);
}
#[test]
fn warm_from_history_preserves_per_scope_isolation() {
let mut idx = QuoteRouteIndex::default();
let items = vec![WarmItem {
scope: "userA@x".into(),
text: "ping\n\n---\nilink-claude".into(),
origin: QuoteOrigin::Client {
vtoken: "vt_a".into(),
name: "ilink-claude".into(),
label: None,
session_name: None,
},
}];
idx.warm_from_history(&items);
let reply_a = quote_reply("userA@x", "ping\n\n---\nilink-claude", None);
let reply_b = quote_reply("userB@x", "ping\n\n---\nilink-claude", None);
assert!(idx.resolve_user_quote("userA@x", &reply_a).is_some());
assert!(idx.resolve_user_quote("userB@x", &reply_b).is_none());
}
#[test]
fn warm_item_from_recent_row_uses_footer_name_and_session() {
let row = crate::store::RecentOutboundRow {
from_user: "user@x".into(),
text: "hello\n\n---\nilink-claude · session-20260611-1".into(),
vtoken: Some("vt".into()),
session_name: "session-20260611-1".into(),
created_at: "2026-06-11 12:00:00".into(),
};
let item = warm_item_from_recent_row(&row).expect("expected Some(WarmItem)");
assert_eq!(item.scope, "user@x");
assert_eq!(item.text, row.text);
match item.origin {
QuoteOrigin::Client {
vtoken,
name,
session_name,
..
} => {
assert_eq!(vtoken, "vt");
assert_eq!(name, "ilink-claude");
assert_eq!(session_name.as_deref(), Some("session-20260611-1"));
}
_ => panic!("expected Client origin"),
}
}
#[test]
fn warm_item_from_recent_row_missing_vtoken_returns_none() {
let row = crate::store::RecentOutboundRow {
from_user: "user@x".into(),
text: "list\n\n---\nhub".into(),
vtoken: None,
session_name: "default".into(),
created_at: "2026-06-11 12:00:00".into(),
};
assert!(warm_item_from_recent_row(&row).is_none());
}
#[test]
fn warm_item_from_recent_row_missing_footer_uses_placeholder_name() {
let row = crate::store::RecentOutboundRow {
from_user: "user@x".into(),
text: "no footer here".into(),
vtoken: Some("vt".into()),
session_name: "session-20260611-1".into(),
created_at: "2026-06-11 12:00:00".into(),
};
let item = warm_item_from_recent_row(&row).expect("expected Some(WarmItem)");
match item.origin {
QuoteOrigin::Client {
vtoken,
name,
session_name,
..
} => {
assert_eq!(vtoken, "vt");
assert_eq!(name, "<warmup>");
assert_eq!(session_name.as_deref(), Some("session-20260611-1"));
}
_ => panic!("expected Client origin with placeholder name"),
}
}
}