wisp-core 0.1.0

core domain model and projections for Wisp
Documentation
mod action;
mod candidate;
mod domain;
mod preview;
mod reduce;
mod view;

pub use action::{Action, CandidateAction, ResolvedAction, resolve_action, sanitize_session_name};
pub use candidate::{
    Candidate, CandidateId, CandidateKind, CandidateMetadata, DirectoryMetadata, ScoreHints,
    SessionMetadata, WindowMetadata, WorktreeMetadata, deduplicate_candidates,
    normalize_display_path, sort_candidates,
};
pub use domain::{
    AlertAggregate, AlertState, AttentionBadge, ClientFocus, ClientId, DirectoryRecord,
    DomainConfig, DomainSnapshot, DomainState, NotificationConfig, PaneId, PaneRecord, SessionId,
    SessionRecord, SessionSortKey, WindowId, WindowRecord, aggregate_alerts,
};
pub use preview::{
    PreviewContent, PreviewKey, PreviewKind, PreviewRequest, preview_request_for_candidate,
};
pub use reduce::{DomainEvent, reduce_domain_event};
pub use view::{
    GitBranchStatus, GitBranchSync, PickerMode, SessionListItem, SessionListItemKind,
    SessionListSortMode, StatusSessionItem, WorktreeInfo, derive_candidates, derive_session_list,
    derive_session_list_with_worktrees, derive_status_items, sort_session_list_items,
};

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use crate::candidate::WorktreeMetadata;
    use crate::{
        Candidate, CandidateAction, CandidateId, CandidateMetadata, DirectoryMetadata, PreviewKey,
        PreviewKind, ResolvedAction, ScoreHints, SessionMetadata, deduplicate_candidates,
        normalize_display_path, preview_request_for_candidate, resolve_action, sort_candidates,
    };

    #[test]
    fn normalizes_home_directory_display_paths() {
        let path = PathBuf::from("/Users/emma/projects/wisp");
        let home = PathBuf::from("/Users/emma");

        let display = normalize_display_path(&path, Some(home.as_path()));

        assert_eq!(display, "~/projects/wisp");
    }

    #[test]
    fn deduplicates_candidates_by_identity_while_keeping_the_better_match() {
        let duplicate_a = Candidate::directory(DirectoryMetadata {
            full_path: PathBuf::from("/tmp/wisp"),
            display_path: "/tmp/wisp".to_string(),
            zoxide_score: Some(5.0),
            git_root_hint: None,
            exists: true,
        });
        let duplicate_b = Candidate {
            score_hints: ScoreHints {
                source_score: Some(9),
                ..ScoreHints::default()
            },
            ..duplicate_a.clone()
        };

        let deduplicated = deduplicate_candidates([duplicate_a, duplicate_b]);

        assert_eq!(deduplicated.len(), 1);
        assert_eq!(deduplicated[0].score_hints.source_score, Some(9));
    }

    #[test]
    fn keeps_worktrees_distinct_from_directories_when_deduplicating() {
        let directory = Candidate::directory(DirectoryMetadata {
            full_path: PathBuf::from("/tmp/wisp"),
            display_path: "/tmp/wisp".to_string(),
            zoxide_score: Some(5.0),
            git_root_hint: None,
            exists: true,
        });
        let worktree = Candidate::worktree(WorktreeMetadata {
            full_path: PathBuf::from("/tmp/wisp"),
            display_path: "/tmp/wisp".to_string(),
            branch: Some("feature/demo".to_string()),
        });

        let deduplicated = deduplicate_candidates([directory, worktree]);

        assert_eq!(deduplicated.len(), 2);
        assert!(deduplicated.iter().any(|candidate| matches!(
            candidate.id,
            CandidateId::Directory(ref path) if path == &PathBuf::from("/tmp/wisp")
        )));
        assert!(deduplicated.iter().any(|candidate| matches!(
            candidate.id,
            CandidateId::Worktree(ref path) if path == &PathBuf::from("/tmp/wisp")
        )));
    }

    #[test]
    fn resolves_actions_from_candidate_metadata() {
        let session = Candidate::session(SessionMetadata {
            session_name: "workbench".to_string(),
            attached: true,
            current: true,
            window_count: 4,
            last_activity: Some(42),
        });
        let directory = Candidate::directory(DirectoryMetadata {
            full_path: PathBuf::from("/Users/emma/projects/wisp"),
            display_path: "~/projects/wisp".to_string(),
            zoxide_score: Some(8.5),
            git_root_hint: None,
            exists: true,
        });

        assert_eq!(
            resolve_action(&session),
            Some(ResolvedAction::SwitchSession {
                session_name: "workbench".to_string(),
            })
        );
        assert_eq!(
            resolve_action(&directory),
            Some(ResolvedAction::CreateOrSwitchSession {
                session_name: "wisp".to_string(),
                directory: PathBuf::from("/Users/emma/projects/wisp"),
            })
        );
    }

    #[test]
    fn derives_preview_keys_from_candidates() {
        let candidate = Candidate::session(SessionMetadata {
            session_name: "ops".to_string(),
            attached: false,
            current: false,
            window_count: 2,
            last_activity: None,
        });

        let request = preview_request_for_candidate(&candidate);

        assert_eq!(request.key(), &PreviewKey::Session("ops".to_string()));
        assert_eq!(request.kind(), PreviewKind::SessionSummary);
    }

    #[test]
    fn sorts_current_and_high_signal_candidates_first() {
        let mut candidates = vec![
            Candidate::directory(DirectoryMetadata {
                full_path: PathBuf::from("/tmp/zeta"),
                display_path: "/tmp/zeta".to_string(),
                zoxide_score: Some(1.0),
                git_root_hint: None,
                exists: true,
            }),
            Candidate::session(SessionMetadata {
                session_name: "alpha".to_string(),
                attached: false,
                current: true,
                window_count: 1,
                last_activity: Some(1),
            }),
        ];

        sort_candidates(&mut candidates);

        assert!(matches!(
            candidates[0].metadata,
            CandidateMetadata::Session(SessionMetadata { current: true, .. })
        ));
    }

    #[test]
    fn exposes_candidate_action_on_domain_values() {
        let candidate = Candidate::directory(DirectoryMetadata {
            full_path: PathBuf::from("/tmp/project"),
            display_path: "/tmp/project".to_string(),
            zoxide_score: Some(3.0),
            git_root_hint: None,
            exists: true,
        });

        assert_eq!(candidate.action, CandidateAction::CreateOrSwitchSession);
    }
}