//! Favorites, notes and personal tags (issue #39). mod common; use agent_manager::commands::annotations_cmd; use agent_manager::commands::search_cmd; use agent_manager::events::{self, EventKind}; use agent_manager::state::{Annotation, InstalledEntry}; fn install(app: &agent_manager::app::App, name: &str) { let entry = InstalledEntry { name: name.to_string(), version: Some("1.0.0".to_string()), method: "npm".to_string(), install_dir: format!("/tmp/{name}"), bins: vec![], run: name.to_string(), installed_at: "2026-08-01T00:00:00Z".to_string(), updated_at: None, pid: None, started_at: None, }; app.state.set(&entry).unwrap(); } #[test] fn favorite_persists_in_state_and_survives_reload() { let app = common::test_app("fav", &["list"]); install(&app, "claude-code"); let code = annotations_cmd::run_favorite(&app, "claude-code").unwrap(); assert_eq!(code, 0); assert!(annotations_cmd::is_favorite(&app, "claude-code")); // A fresh StateStore over the same file sees the star (restart). let store = agent_manager::state::StateStore::new(app.state.path().to_path_buf()); let ann = store.annotation("claude-code").unwrap(); assert!(ann.favorite, "favorite must survive a reload"); let code = annotations_cmd::run_unfavorite(&app, "claude-code").unwrap(); assert_eq!(code, 0); assert!(!annotations_cmd::is_favorite(&app, "claude-code")); } #[test] fn favorite_is_journalized() { let app = common::test_app("favlog", &["list"]); install(&app, "claude-code"); annotations_cmd::run_favorite(&app, "claude-code").unwrap(); let evs = events::read_events(&app.events_dir(), 10); assert!( evs.iter().any(|e| e.kind == EventKind::Annotate && e.agent.as_deref() == Some("claude-code")), "an annotate event must be journaled: {evs:?}" ); } #[test] fn note_is_stored_and_shown() { let app = common::test_app("note", &["list"]); install(&app, "claude-code"); let code = annotations_cmd::run_note( &app, "claude-code", &["use".to_string(), "the".to_string(), "sonnet".to_string(), "model".to_string()], ) .unwrap(); assert_eq!(code, 0); let ann = app.state.annotation("claude-code").unwrap(); assert_eq!(ann.note.as_deref(), Some("use the sonnet model")); // Showing the note is a no-error operation too. assert_eq!(annotations_cmd::run_note(&app, "claude-code", &[]).unwrap(), 0); } #[test] fn tags_are_filterable_in_search() { let app = common::test_app("tag", &["list"]); install(&app, "claude-code"); install(&app, "gemini-cli"); annotations_cmd::run_tag(&app, "claude-code", &["python".to_string(), "Frontend".to_string()]).unwrap(); annotations_cmd::run_tag(&app, "gemini-cli", &["python".to_string()]).unwrap(); // Personal tags are lowercased and de-duplicated. let ann: Annotation = app.state.annotation("claude-code").unwrap(); assert_eq!(ann.tags, vec!["frontend", "python"]); // search --tag python returns both tagged agents. assert_eq!( annotations_cmd::agents_with_tag(&app, "python"), vec!["claude-code".to_string(), "gemini-cli".to_string()] ); let code = search_cmd::run(&app, Some(""), None, Some("python")).unwrap(); assert_eq!(code, 0); // A tag matching nothing is reported cleanly. let code = search_cmd::run(&app, Some(""), None, Some("nope")).unwrap(); assert_eq!(code, 0); // untag removes one tag; tags command lists the rest. annotations_cmd::run_untag(&app, "claude-code", "frontend").unwrap(); let ann = app.state.annotation("claude-code").unwrap(); assert_eq!(ann.tags, vec!["python"]); let code = annotations_cmd::run_tags(&app, None).unwrap(); assert_eq!(code, 0); let code = annotations_cmd::run_tags(&app, Some("claude-code")).unwrap(); assert_eq!(code, 0); } #[test] fn tag_requires_at_least_one_word() { let app = common::test_app("tagempty", &["list"]); install(&app, "claude-code"); let err = annotations_cmd::run_tag(&app, "claude-code", &[]).unwrap_err().to_string(); assert!(err.contains("usage"), "{err}"); } #[test] fn unknown_agent_is_rejected() { let app = common::test_app("favbad", &["list"]); let err = annotations_cmd::run_favorite(&app, "does-not-exist").unwrap_err().to_string(); assert!(err.contains("unknown agent"), "{err}"); }