//! Tests for `recall.rs`, split out to keep the production file under the //! project's file-length guideline. use super::*; use crate::campaign::{Campaign, GameTime}; use serde_json::json; use tempfile::TempDir; /// A recall tool backed by a campaign holding two marks and a narration. fn seeded_tool() -> (RecallTool, TempDir) { let dir = TempDir::new().unwrap(); let campaign = Campaign::open(dir.path()).unwrap(); campaign .mark( GameTime::parse("#d1-0830").unwrap(), "The party meets a goblin.", Some("A goblin blocks the road."), ) .unwrap(); campaign .append_narration( GameTime::parse("#d1-0900").unwrap(), "The goblin snarls and draws a rusty blade.", ) .unwrap(); (RecallTool::new(campaign), dir) } fn call(tool: &mut RecallTool, args: Value) -> ToolReply { tool.call(&args, Visibility::Public).unwrap() } #[test] fn keyword_call_finds_matching_entries_with_their_time_and_source() { let (mut tool, _dir) = seeded_tool(); let reply = call(&mut tool, json!({ "keywords": ["goblin"] })); assert_eq!( reply.for_model, "- #d1-0900 [transcript]: The goblin snarls and draws a rusty blade.\n- #d1-0830 [campaign-log]: The party meets a goblin.\n- #d1-0830 [transcript]: A goblin blocks the road." ); } #[test] fn matching_is_case_insensitive() { let (mut tool, _dir) = seeded_tool(); let reply = call(&mut tool, json!({ "keywords": ["GOBLIN"] })); assert_eq!( reply.for_model, "- #d1-0900 [transcript]: The goblin snarls and draws a rusty blade.\n- #d1-0830 [campaign-log]: The party meets a goblin.\n- #d1-0830 [transcript]: A goblin blocks the road." ); } #[test] fn the_public_line_counts_the_matches() { let (mut tool, _dir) = seeded_tool(); let reply = call(&mut tool, json!({ "keywords": ["goblin"] })); assert_eq!(reply.public, Text::raw("🔍 recall \"goblin\" → 3 matches")); } #[test] fn a_screened_recall_shows_the_screened_line() { let (mut tool, _dir) = seeded_tool(); let reply = tool .call(&json!({ "keywords": ["goblin"] }), Visibility::Screened) .unwrap(); assert_eq!( reply.screened, Text::from(Line::from(Span::styled(SCREENED_LINE, dim_style()))) ); } #[test] fn a_source_filter_restricts_the_search() { let (mut tool, _dir) = seeded_tool(); let reply = call( &mut tool, json!({ "keywords": ["goblin"], "source": "transcript" }), ); assert_eq!( reply.for_model, "- #d1-0900 [transcript]: The goblin snarls and draws a rusty blade.\n- #d1-0830 [transcript]: A goblin blocks the road." ); } #[test] fn an_invalid_source_is_rejected() { let (mut tool, _dir) = seeded_tool(); let error = tool .call( &json!({ "keywords": ["goblin"], "source": "lore" }), Visibility::Public, ) .unwrap_err(); assert!(error.contains("`source`")); assert!(error.contains("campaign-log")); } #[test] fn no_match_names_what_to_try_next() { let (mut tool, _dir) = seeded_tool(); let reply = call(&mut tool, json!({ "keywords": ["dragon"] })); assert!(reply.for_model.contains("No entries matched")); assert_eq!(reply.public, Text::raw("🔍 recall \"dragon\" → no matches")); } #[test] fn missing_keywords_are_rejected() { let (mut tool, _dir) = seeded_tool(); let error = tool.call(&json!({}), Visibility::Public).unwrap_err(); assert!(error.contains("`keywords` is required")); } #[test] fn non_array_keywords_are_rejected() { let (mut tool, _dir) = seeded_tool(); let error = tool .call(&json!({ "keywords": "goblin" }), Visibility::Public) .unwrap_err(); assert!(error.contains("must be an array of strings")); } #[test] fn empty_keywords_are_rejected() { let (mut tool, _dir) = seeded_tool(); let error = tool .call(&json!({ "keywords": [] }), Visibility::Public) .unwrap_err(); assert!(error.contains("must not be empty")); } #[test] fn a_non_string_keyword_is_rejected() { let (mut tool, _dir) = seeded_tool(); let error = tool .call(&json!({ "keywords": [5] }), Visibility::Public) .unwrap_err(); assert!(error.contains("every keyword must be a string")); } #[test] fn a_non_string_source_is_rejected() { let (mut tool, _dir) = seeded_tool(); let error = tool .call( &json!({ "keywords": ["goblin"], "source": 5 }), Visibility::Public, ) .unwrap_err(); assert!(error.contains("`source` was `5`")); } #[test] fn a_single_match_is_counted_as_one() { let (mut tool, _dir) = seeded_tool(); let reply = call(&mut tool, json!({ "keywords": ["rusty"] })); assert_eq!(reply.public, Text::raw("🔍 recall \"rusty\" → 1 match")); } #[test] fn the_definition_names_the_tool() { let (tool, _dir) = seeded_tool(); assert_eq!(tool.definition()["function"]["name"], "recall"); }