use super::*;
#[test]
fn resume_by_name_matches_titles_and_refuses_ambiguity() {
let s = |id: &str, title: &str| newt_core::ConversationSummary {
id: id.into(),
title: title.into(),
persona: None,
updated_at_unix_nanos: 0,
turn_count: 1,
};
let list = vec![
s("aaaa1111-0000-0000-0000-000000000000", "mesh docking"),
s(
"bbbb2222-0000-0000-0000-000000000000",
"Mesh Docking Ceremony",
),
s("cccc3333-0000-0000-0000-000000000000", "taxes TY2025"),
];
assert_eq!(
resolve_conversation_by_name(&list, "Mesh Docking").unwrap(),
"aaaa1111-0000-0000-0000-000000000000"
);
assert_eq!(
resolve_conversation_by_name(&list, "taxes").unwrap(),
"cccc3333-0000-0000-0000-000000000000"
);
let err = resolve_conversation_by_name(&list, "docking")
.unwrap_err()
.to_string();
assert!(err.contains("matches 2 conversations"), "{err}");
assert!(err.contains("mesh docking"), "{err}");
let err = resolve_conversation_by_name(&list, "nonesuch")
.unwrap_err()
.to_string();
assert!(err.contains("no conversation titled"), "{err}");
}
#[test]
fn resolve_resume_target_chains_id_prefix_title_then_ambiguity() {
let s = |id: &str, title: &str| newt_core::ConversationSummary {
id: id.into(),
title: title.into(),
persona: None,
updated_at_unix_nanos: 0,
turn_count: 1,
};
let list = vec![
s("aaaa1111-0000-0000-0000-000000000000", "mesh docking"),
s(
"bbbb2222-0000-0000-0000-000000000000",
"Mesh Docking Ceremony",
),
s("cccc3333-0000-0000-0000-000000000000", "taxes TY2025"),
];
assert_eq!(
crate::resolve_resume_target(&list, "cccc3333-0000-0000-0000-000000000000"),
crate::ResumeNameResolve::Resolved("cccc3333-0000-0000-0000-000000000000".into())
);
assert_eq!(
crate::resolve_resume_target(&list, "aaaa1111"),
crate::ResumeNameResolve::Resolved("aaaa1111-0000-0000-0000-000000000000".into())
);
assert_eq!(
crate::resolve_resume_target(&list, "mesh docking"),
crate::ResumeNameResolve::Resolved("aaaa1111-0000-0000-0000-000000000000".into())
);
assert_eq!(
crate::resolve_resume_target(&list, "taxes"),
crate::ResumeNameResolve::Resolved("cccc3333-0000-0000-0000-000000000000".into())
);
let amb = crate::resolve_resume_target(&list, "docking");
let cands = match amb {
crate::ResumeNameResolve::Ambiguous(c) => c,
other => panic!("expected Ambiguous, got {other:?}"),
};
assert_eq!(cands.len(), 2);
assert!(cands
.iter()
.any(|(id, _)| id == "aaaa1111-0000-0000-0000-000000000000"));
assert!(cands
.iter()
.any(|(id, _)| id == "bbbb2222-0000-0000-0000-000000000000"));
assert_eq!(
crate::resolve_resume_target(&list, "nonesuch"),
crate::ResumeNameResolve::NotFound
);
let shared_prefix = vec![
s("aaaa1111-0000-0000-0000-000000000000", "one"),
s("aaaa2222-0000-0000-0000-000000000000", "two"),
];
assert_eq!(
crate::resolve_resume_target(&shared_prefix, "aaaa"),
crate::ResumeNameResolve::NotFound
);
}
#[test]
fn resolve_resume_target_agrees_with_resolve_conversation_by_name_on_titles() {
let s = |id: &str, title: &str| newt_core::ConversationSummary {
id: id.into(),
title: title.into(),
persona: None,
updated_at_unix_nanos: 0,
turn_count: 1,
};
let list = vec![
s("aaaa1111-0000-0000-0000-000000000000", "mesh docking"),
s(
"bbbb2222-0000-0000-0000-000000000000",
"Mesh Docking Ceremony",
),
s("cccc3333-0000-0000-0000-000000000000", "taxes TY2025"),
];
for q in [
"mesh docking",
"Mesh Docking",
"taxes",
"docking",
"nonesuch",
] {
let via_target = crate::resolve_resume_target(&list, q);
let via_name = resolve_conversation_by_name(&list, q);
match (&via_target, via_name) {
(crate::ResumeNameResolve::Resolved(a), Ok(b)) => assert_eq!(a, &b, "title {q:?}"),
(
crate::ResumeNameResolve::Ambiguous(a),
Err(crate::TitleResolveError::Ambiguous { candidates: b, .. }),
) => {
let a_ids: Vec<_> = a.iter().map(|(id, _)| id.as_str()).collect();
let b_ids: Vec<_> = b.iter().map(|(id, _)| id.as_str()).collect();
assert_eq!(a_ids, b_ids, "ambiguous title {q:?}");
}
(
crate::ResumeNameResolve::NotFound,
Err(crate::TitleResolveError::NotFound { .. }),
) => {}
other => panic!("disagreement on {q:?}: {other:?}"),
}
}
}
#[serial_test::serial(real_fs)]
#[test]
fn resume_ambiguous_message_numbers_candidates_for_selection() {
let (_state, _ws, store) = recall_test_store();
let a = store.create("mesh docking", None).unwrap();
let b = store.create("Mesh Docking Ceremony", None).unwrap();
let cands = vec![
(a.clone(), "mesh docking".to_string()),
(b.clone(), "Mesh Docking Ceremony".to_string()),
];
let (msg, ids) = resume_ambiguous_message(&store, "docking", &cands, "other-active").unwrap();
assert_eq!(ids, vec![a.clone(), b.clone()]);
assert!(msg.contains("\"docking\" matches 2 conversations"), "{msg}");
assert!(msg.contains("1. "), "numbered: {msg}");
assert!(msg.contains("2. "), "numbered: {msg}");
assert!(msg.contains("mesh docking") && msg.contains("Mesh Docking Ceremony"));
}
#[serial_test::serial(real_fs)]
#[test]
fn resume_refuses_a_conversation_a_live_newt_owns() {
let (_state, _ws, mut store) = recall_test_store();
let id = store.create("Held by another", None).unwrap();
store.set_owner_for_test("hostA", "bootA", 1);
store.set_liveness_for_test(|_, _| true);
assert_eq!(
store.claim(&id).unwrap(),
newt_core::ClaimOutcome::Claimed,
"first claim by host A should acquire"
);
store.set_owner_for_test("hostB", "bootB", 2);
match store.claim(&id) {
Ok(newt_core::ClaimOutcome::HeldBy { host, pid }) => {
assert_eq!(host, "hostA");
assert_eq!(pid, 1);
}
other => panic!("expected HeldBy for a live-owned conversation, got {other:?}"),
}
}
#[serial_test::serial(real_fs)]
#[test]
fn should_auto_resume_only_for_latest_and_never_after_new() {
assert!(should_auto_resume(&SessionStart::ResumeLatest, false));
assert!(!should_auto_resume(&SessionStart::Fresh, false));
assert!(!should_auto_resume(&SessionStart::Ephemeral, false));
assert!(!should_auto_resume(
&SessionStart::ResumeExact("id".into()),
false
));
assert!(!should_auto_resume(&SessionStart::ResumeLatest, true));
}