quinjet 0.0.35

A fast, live, keyboard-first Git source-control interface for the terminal
use super::*;

#[test]
fn check_status_sections_group_fold_and_navigate() {
    let mut app = App::new("/tmp/repo", "repo");
    let now = Instant::now();
    app.view = View::PullRequests;
    app.focus = Focus::Sidebar;
    app.pull_request = Some(pull_request(8, "Grouped checks", "acme/widget"));
    app.pull_request_checks = vec![
        check("pending", PullRequestCheckStatus::Pending),
        check("broken", PullRequestCheckStatus::Failed),
        check("green", PullRequestCheckStatus::Passed),
        check("skipped", PullRequestCheckStatus::Skipped),
    ];

    let rows = app.check_list_rows();
    assert!(matches!(rows.first(), Some(CheckListRow::Conversation)));
    assert!(rows.iter().any(|row| matches!(row, CheckListRow::Heading)));
    assert!(rows.iter().any(|row| matches!(
        row,
        CheckListRow::Section {
            section: CheckStatusSection::Failed,
            count: 1,
            collapsed: false,
        }
    )));
    assert!(rows.iter().any(|row| matches!(
        row,
        CheckListRow::Section {
            section: CheckStatusSection::InProgress,
            count: 1,
            collapsed: false,
        }
    )));
    assert!(rows.iter().any(|row| matches!(
        row,
        CheckListRow::Section {
            section: CheckStatusSection::Successful,
            count: 1,
            collapsed: false,
        }
    )));
    assert!(rows.iter().any(|row| matches!(
        row,
        CheckListRow::Section {
            section: CheckStatusSection::Skipped,
            count: 1,
            collapsed: false,
        }
    )));

    app.navigate(1, now);
    assert_eq!(app.selected_check_section, Some(CheckStatusSection::Failed));
    assert!(app.pull_request_check_cursor.is_none());

    assert!(app.toggle_selected_check_section());
    assert!(
        app.collapsed_check_sections
            .contains(&CheckStatusSection::Failed)
    );
    assert!(
        !app.check_list_rows()
            .iter()
            .any(|row| matches!(row, CheckListRow::Check { index: 1 }))
    );

    app.navigate(1, now);
    assert_eq!(
        app.selected_check_section,
        Some(CheckStatusSection::InProgress)
    );
    app.navigate(1, now);
    assert_eq!(app.pull_request_check_cursor, Some(0));
    assert!(app.selected_check_section.is_none());

    app.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), now);
    assert_eq!(
        app.selected_check_section,
        Some(CheckStatusSection::InProgress)
    );
    assert!(app.pull_request_check_cursor.is_none());

    app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), now);
    assert_eq!(app.pull_request_check_cursor, Some(0));

    app.go_to_edge(true, now);
    assert_eq!(app.pull_request_check_cursor, Some(3));
    app.go_to_edge(false, now);
    assert!(app.pull_request_check_cursor.is_none());
    assert!(app.selected_check_section.is_none());
}

#[test]
fn opening_the_repository_picker_does_not_cancel_a_pull_request_lookup() {
    let mut app = App::new("/tmp/repo", "repo");
    let now = Instant::now();
    app.view = View::PullRequests;

    let mut effects = Vec::new();
    app.request_pull_request_lookup(42, false, false, &mut effects);
    let lookup = app.pull_request_generation;
    assert!(app.pull_request_loading);

    app.open_pull_request_repositories(&mut Vec::new());
    assert_eq!(
        app.pull_request_generation, lookup,
        "repository discovery answers on its own counter"
    );

    app.handle_worker_event(
        WorkerEvent::PullRequestLookup {
            generation: lookup,
            result: Ok(crate::git::github::PullRequestSnapshot {
                repositories: Vec::new(),
                selected_repository: None,
                pull_request: pull_request(42, "Still wanted", "acme/widget"),
                warnings: Vec::new(),
                exact_number: Some(42),
                from_cache: false,
            }),
        },
        now,
    );

    assert!(
        !app.pull_request_loading,
        "the lookup reply is still accepted, so polling is never blocked"
    );
    assert_eq!(
        app.pull_request.as_ref().map(|request| request.number),
        Some(42)
    );
}

#[test]
fn moving_to_another_check_never_shows_the_previous_run_under_its_name() {
    let mut app = App::new("/tmp/repo", "repo");
    let now = Instant::now();
    app.view = View::PullRequests;
    app.pull_request = Some(pull_request(8, "Two checks", "acme/widget"));
    app.pull_request_checks = vec![
        check("Build every workspace", PullRequestCheckStatus::Passed),
        check("No-comment policy", PullRequestCheckStatus::Passed),
    ];

    let mut effects = Vec::new();
    app.select_pull_request_check(Some(0), &mut effects);
    assert_eq!(effects.len(), 1);
    let slow = app.pull_request_check_log_generation;
    assert!(app.pull_request_check_log_loading);

    let mut effects = Vec::new();
    app.select_pull_request_check(Some(1), &mut effects);
    assert!(
        matches!(
            effects.as_slice(),
            [AppEffect::Git(command)] if matches!(
                command.as_ref(),
                WorkerCommand::LoadCheckRunLog { check, .. }
                    if check.name == "No-comment policy"
            )
        ),
        "the newly selected run is requested rather than waited for"
    );
    assert_ne!(
        app.pull_request_check_log_generation, slow,
        "the in-flight read is invalidated by the move"
    );

    let effects = app.handle_worker_event(
        WorkerEvent::CheckRunLog {
            generation: slow,
            result: Ok(CheckRunLog {
                steps: vec![crate::git::github::CheckStep {
                    number: 1,
                    name: "Build every workspace".to_owned(),
                    status: PullRequestCheckStatus::Passed,
                    conclusion: "success".to_owned(),
                    started_at: String::new(),
                    completed_at: String::new(),
                    lines: Vec::new(),
                }],
                loose_lines: Vec::new(),
                truncated: false,
                unavailable: None,
                log_pending: false,
            }),
        },
        now,
    );

    assert!(effects.is_empty());
    assert!(
        app.pull_request_check_log.is_none(),
        "a reply for the previous check is discarded, not shown under the new one"
    );
    assert!(
        app.pull_request_check_log_loading,
        "the new read is still pending"
    );
}

#[test]
fn a_running_log_follows_its_own_tail_unless_the_reader_scrolled_up() {
    let mut app = App::new("/tmp/repo", "repo");
    let now = Instant::now();
    app.view = View::PullRequests;
    app.pull_request = Some(pull_request(8, "Running", "acme/widget"));
    app.pull_request_checks = vec![check("build", PullRequestCheckStatus::Pending)];
    app.pull_request_check_cursor = Some(0);
    app.pull_request_check_log_generation = 3;
    let log = || CheckRunLog {
        steps: Vec::new(),
        loose_lines: Vec::new(),
        truncated: false,
        unavailable: None,
        log_pending: false,
    };

    app.content_at_bottom = true;
    app.content_scroll = 120;
    app.handle_worker_event(
        WorkerEvent::CheckRunLog {
            generation: 3,
            result: Ok(log()),
        },
        now,
    );
    assert_eq!(
        app.content_scroll,
        usize::MAX,
        "the draw clamps this to the new end"
    );

    app.content_at_bottom = false;
    app.content_scroll = 40;
    app.pull_request_check_log_generation = 4;
    app.handle_worker_event(
        WorkerEvent::CheckRunLog {
            generation: 4,
            result: Ok(log()),
        },
        now,
    );
    assert_eq!(app.content_scroll, 40);

    app.pull_request_checks = vec![check("build", PullRequestCheckStatus::Passed)];
    app.content_at_bottom = true;
    app.content_scroll = 40;
    app.pull_request_check_log_generation = 5;
    app.handle_worker_event(
        WorkerEvent::CheckRunLog {
            generation: 5,
            result: Ok(log()),
        },
        now,
    );
    assert_eq!(app.content_scroll, 40);
}

#[test]
fn a_settled_pull_request_polls_less_often_than_a_running_one() {
    let mut app = App::new("/tmp/repo", "repo");
    app.view = View::PullRequests;
    app.pull_request = Some(pull_request(8, "Checks", "acme/widget"));
    app.pull_request_checks = vec![check("build", PullRequestCheckStatus::Passed)];
    assert_eq!(app.pull_request_poll_interval(), PULL_REQUEST_IDLE_POLL);

    app.pull_request_checks = vec![check("build", PullRequestCheckStatus::Pending)];
    assert_eq!(app.pull_request_poll_interval(), PULL_REQUEST_ACTIVE_POLL);

    app.view = View::Changes;
    assert_eq!(
        app.pull_request_poll_interval(),
        PULL_REQUEST_BACKGROUND_POLL,
        "a pull request nobody is looking at still stays fresh, just cheaply"
    );
}

#[test]
fn a_moved_head_reindexes_the_diff_and_keeps_everything_else() {
    let mut app = App::new("/tmp/repo", "repo");
    let now = Instant::now();
    app.view = View::PullRequests;
    app.pull_request_generation = 4;
    app.pull_request_loading = true;
    app.pull_request = Some(pull_request(8, "Force pushed", "acme/widget"));
    app.pull_request_section = PullRequestSection::Files;
    app.pull_request_workspace_generation = Some(2);
    app.pull_request_documents
        .insert(PathBuf::from("src/one.rs"), DiffDocument::default());
    app.pull_request_check_cursor = Some(0);
    app.pull_request_checks = vec![check("build", PullRequestCheckStatus::Passed)];

    let mut moved = pull_request(8, "Force pushed", "acme/widget");
    moved.head_oid = "rewritten".to_owned();
    let repository = moved.base_repository.clone();
    let effects = app.handle_worker_event(
        WorkerEvent::PullRequestLookup {
            generation: 4,
            result: Ok(crate::git::github::PullRequestSnapshot {
                repositories: vec![repository.clone()],
                selected_repository: Some(repository),
                pull_request: moved,
                warnings: Vec::new(),
                exact_number: Some(8),
                from_cache: false,
            }),
        },
        now,
    );

    assert!(app.pull_request_workspace_generation.is_none());
    assert!(app.pull_request_documents.is_empty());
    assert_eq!(
        app.pull_request_section,
        PullRequestSection::Files,
        "a force push replaces the diff, not the reader's place in the view"
    );
    assert_eq!(app.pull_request_check_cursor, Some(0));
    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::PreparePullRequest { .. })
    )));
}

#[test]
fn a_forwarded_webhook_refreshes_immediately_instead_of_waiting_for_the_poll() {
    let mut app = App::new("/tmp/repo", "repo");
    let now = Instant::now();
    app.view = View::PullRequests;
    app.pull_request = Some(pull_request(8, "Checks", "acme/widget"));
    app.pull_request_exact_number = Some(8);
    app.pull_request_poll_due = Some(now + Duration::from_secs(3_600));

    let effects = app.webhook_delivered(now);

    assert!(effects.iter().any(|effect| matches!(
        effect,
        AppEffect::Git(command)
            if matches!(command.as_ref(), WorkerCommand::LoadPullRequestChecks { .. })
    )));
    assert!(
        app.pull_request_poll_due
            .is_some_and(|due| due <= now + PULL_REQUEST_IDLE_POLL),
        "the delivery also restarts the poll clock"
    );
}

#[test]
fn history_reset_requested_during_a_load_runs_after_the_in_flight_page() {
    let mut app = App::new("/tmp/repo", "repo");
    app.history_generation = 4;
    app.history_loading = true;
    let mut effects = Vec::new();

    app.request_history(true, &mut effects);
    assert!(effects.is_empty());
    assert!(app.history_refresh_again);

    let effects = app.handle_worker_event(
        WorkerEvent::History {
            generation: 4,
            skip: 0,
            result: Ok(Vec::new()),
        },
        Instant::now(),
    );

    assert!(matches!(
        effects.as_slice(),
        [AppEffect::Git(command)] if matches!(
            command.as_ref(),
            WorkerCommand::LoadHistory {
                generation: 5,
                skip: 0,
                limit: HISTORY_PAGE_SIZE,
                ..
            }
        )
    ));
    assert!(app.history_loading);
    assert!(!app.history_refresh_again);
}