a73x

tests/sort_test.rs

Ref:   Size: 11.1 KiB

mod common;

use common::{alice, bob, init_repo, test_signing_key, TestRepo};
use git2::Repository;
use tempfile::TempDir;

use git_collab::cli::SortMode;
use git_collab::dag;
use git_collab::event::{Action, Author, Event};
use git_collab::state::{self, IssueState, PatchState};

/// Create an issue with a specific timestamp. Returns (ref_name, id).
fn open_issue_at(
    repo: &Repository,
    author: &Author,
    title: &str,
    timestamp: &str,
) -> (String, String) {
    let sk = test_signing_key();
    let event = Event {
        timestamp: timestamp.to_string(),
        author: author.clone(),
        action: Action::IssueOpen {
            title: title.to_string(),
            body: "".to_string(),
            relates_to: None,
        },
        clock: 0,
    };
    let oid = dag::create_root_event(repo, &event, &sk).unwrap();
    let id = oid.to_string();
    let ref_name = format!("refs/collab/issues/{}", id);
    repo.reference(&ref_name, oid, false, "test open").unwrap();
    (ref_name, id)
}

/// Add a comment with a specific timestamp to an issue.
fn add_comment_at(
    repo: &Repository,
    ref_name: &str,
    author: &Author,
    body: &str,
    timestamp: &str,
) {
    let sk = test_signing_key();
    let event = Event {
        timestamp: timestamp.to_string(),
        author: author.clone(),
        action: Action::IssueComment {
            body: body.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, ref_name, &event, &sk).unwrap();
}

/// Create a patch with a specific timestamp. Returns (ref_name, id).
fn create_patch_at(
    repo: &Repository,
    author: &Author,
    title: &str,
    timestamp: &str,
) -> (String, String) {
    let sk = test_signing_key();
    let event = Event {
        timestamp: timestamp.to_string(),
        author: author.clone(),
        action: Action::PatchCreate {
            title: title.to_string(),
            body: "".to_string(),
            base_ref: "main".to_string(),
            branch: format!("branch-{}", title.replace(' ', "-")),
            fixes: None,
            commit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
            tree: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
            base_commit: None,
        },
        clock: 0,
    };
    let oid = dag::create_root_event(repo, &event, &sk).unwrap();
    let id = oid.to_string();
    let ref_name = format!("refs/collab/patches/{}", id);
    repo.reference(&ref_name, oid, false, "test patch").unwrap();
    (ref_name, id)
}

/// Add a comment to a patch with a specific timestamp.
fn add_patch_comment_at(
    repo: &Repository,
    ref_name: &str,
    author: &Author,
    body: &str,
    timestamp: &str,
) {
    let sk = test_signing_key();
    let event = Event {
        timestamp: timestamp.to_string(),
        author: author.clone(),
        action: Action::PatchComment {
            body: body.to_string(),
        },
        clock: 0,
    };
    dag::append_event(repo, ref_name, &event, &sk).unwrap();
}

// ---- Issue sorting tests ----

#[test]
fn test_issue_last_updated_populated() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    let (ref_name, id) = open_issue_at(&repo, &alice(), "Test issue", "2025-01-01T00:00:00Z");
    add_comment_at(
        &repo,
        &ref_name,
        &bob(),
        "later comment",
        "2025-06-15T12:00:00Z",
    );

    let state = IssueState::from_ref(&repo, &ref_name, &id).unwrap();
    assert_eq!(state.last_updated, "2025-06-15T12:00:00Z");
    assert_eq!(state.created_at, "2025-01-01T00:00:00Z");
}

#[test]
fn test_issue_default_sort_by_recency() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    // Issue A: created early, updated recently
    let (ref_a, _) = open_issue_at(&repo, &alice(), "Alpha issue", "2025-01-01T00:00:00Z");
    add_comment_at(
        &repo,
        &ref_a,
        &bob(),
        "recent comment",
        "2025-12-01T00:00:00Z",
    );

    // Issue B: created later, never updated after creation
    let (_, _) = open_issue_at(&repo, &alice(), "Beta issue", "2025-06-01T00:00:00Z");

    let issues = state::list_issues(&repo).unwrap();
    assert_eq!(issues.len(), 2);

    // Default sort = recent: issue A (last_updated=2025-12) should come first
    let entries = git_collab::issue::list(&repo, true, false, None, None, SortMode::Recent).unwrap();
    assert_eq!(entries.len(), 2);
    assert_eq!(entries[0].issue.title, "Alpha issue");
    assert_eq!(entries[1].issue.title, "Beta issue");
}

#[test]
fn test_issue_sort_by_created() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    // Issue A: created early, updated recently
    let (ref_a, _) = open_issue_at(&repo, &alice(), "Alpha issue", "2025-01-01T00:00:00Z");
    add_comment_at(
        &repo,
        &ref_a,
        &bob(),
        "recent comment",
        "2025-12-01T00:00:00Z",
    );

    // Issue B: created later
    let (_, _) = open_issue_at(&repo, &alice(), "Beta issue", "2025-06-01T00:00:00Z");

    // Sort by created: B (2025-06) comes first (descending)
    let entries = git_collab::issue::list(&repo, true, false, None, None, SortMode::Created).unwrap();
    assert_eq!(entries.len(), 2);
    assert_eq!(entries[0].issue.title, "Beta issue");
    assert_eq!(entries[1].issue.title, "Alpha issue");
}

#[test]
fn test_issue_sort_alpha() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    open_issue_at(&repo, &alice(), "Zebra issue", "2025-01-01T00:00:00Z");
    open_issue_at(&repo, &alice(), "Apple issue", "2025-06-01T00:00:00Z");
    open_issue_at(&repo, &alice(), "Mango issue", "2025-03-01T00:00:00Z");

    let entries = git_collab::issue::list(&repo, true, false, None, None, SortMode::Alpha).unwrap();
    assert_eq!(entries.len(), 3);
    assert_eq!(entries[0].issue.title, "Apple issue");
    assert_eq!(entries[1].issue.title, "Mango issue");
    assert_eq!(entries[2].issue.title, "Zebra issue");
}

// ---- Patch sorting tests ----

#[test]
fn test_patch_last_updated_populated() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    let (ref_name, id) =
        create_patch_at(&repo, &alice(), "Test patch", "2025-01-01T00:00:00Z");
    add_patch_comment_at(
        &repo,
        &ref_name,
        &bob(),
        "later comment",
        "2025-06-15T12:00:00Z",
    );

    let state = PatchState::from_ref(&repo, &ref_name, &id).unwrap();
    assert_eq!(state.last_updated, "2025-06-15T12:00:00Z");
    assert_eq!(state.created_at, "2025-01-01T00:00:00Z");
}

#[test]
fn test_patch_default_sort_by_recency() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    // Patch A: created early, updated recently
    let (ref_a, _) = create_patch_at(&repo, &alice(), "Alpha patch", "2025-01-01T00:00:00Z");
    add_patch_comment_at(
        &repo,
        &ref_a,
        &bob(),
        "recent comment",
        "2025-12-01T00:00:00Z",
    );

    // Patch B: created later, never updated
    let (_, _) = create_patch_at(&repo, &alice(), "Beta patch", "2025-06-01T00:00:00Z");

    let entries = git_collab::patch::list(&repo, true, false, None, None, SortMode::Recent).unwrap();
    assert_eq!(entries.len(), 2);
    assert_eq!(entries[0].patch.title, "Alpha patch");
    assert_eq!(entries[1].patch.title, "Beta patch");
}

#[test]
fn test_patch_sort_by_created() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    let (ref_a, _) = create_patch_at(&repo, &alice(), "Alpha patch", "2025-01-01T00:00:00Z");
    add_patch_comment_at(
        &repo,
        &ref_a,
        &bob(),
        "recent comment",
        "2025-12-01T00:00:00Z",
    );

    let (_, _) = create_patch_at(&repo, &alice(), "Beta patch", "2025-06-01T00:00:00Z");

    let entries = git_collab::patch::list(&repo, true, false, None, None, SortMode::Created).unwrap();
    assert_eq!(entries.len(), 2);
    assert_eq!(entries[0].patch.title, "Beta patch");
    assert_eq!(entries[1].patch.title, "Alpha patch");
}

#[test]
fn test_patch_sort_alpha() {
    let dir = TempDir::new().unwrap();
    let repo = init_repo(dir.path(), &alice());

    create_patch_at(&repo, &alice(), "Zebra patch", "2025-01-01T00:00:00Z");
    create_patch_at(&repo, &alice(), "Apple patch", "2025-06-01T00:00:00Z");
    create_patch_at(&repo, &alice(), "Mango patch", "2025-03-01T00:00:00Z");

    let entries = git_collab::patch::list(&repo, true, false, None, None, SortMode::Alpha).unwrap();
    assert_eq!(entries.len(), 3);
    assert_eq!(entries[0].patch.title, "Apple patch");
    assert_eq!(entries[1].patch.title, "Mango patch");
    assert_eq!(entries[2].patch.title, "Zebra patch");
}

// ---- CLI integration test ----

#[test]
fn test_cli_issue_list_sort_flag() {
    let repo = TestRepo::new("Alice", "alice@example.com");

    repo.issue_open("Zebra");
    // Small delay to ensure different timestamps
    std::thread::sleep(std::time::Duration::from_millis(50));
    repo.issue_open("Apple");

    // Default sort (recent) should show Apple first (created more recently)
    let out = repo.run_ok(&["issue", "list"]);
    let lines: Vec<&str> = out.lines().collect();
    assert_eq!(lines.len(), 2);
    assert!(lines[0].contains("Apple"), "expected Apple first, got: {}", out);

    // Alpha sort should show Apple first (alphabetical)
    let out = repo.run_ok(&["issue", "list", "--sort", "alpha"]);
    let lines: Vec<&str> = out.lines().collect();
    assert!(lines[0].contains("Apple"), "expected Apple first in alpha sort, got: {}", out);
    assert!(lines[1].contains("Zebra"), "expected Zebra second in alpha sort, got: {}", out);

    // Created sort should show Apple first (most recently created)
    let out = repo.run_ok(&["issue", "list", "--sort", "created"]);
    let lines: Vec<&str> = out.lines().collect();
    assert!(lines[0].contains("Apple"), "expected Apple first in created sort, got: {}", out);
}

#[test]
fn test_cli_patch_list_sort_flag() {
    let repo = TestRepo::new("Alice", "alice@example.com");

    repo.patch_create("Zebra");
    std::thread::sleep(std::time::Duration::from_millis(50));
    repo.patch_create("Apple");

    // Alpha sort
    let out = repo.run_ok(&["patch", "list", "--sort", "alpha"]);
    let lines: Vec<&str> = out.lines().collect();
    assert_eq!(lines.len(), 2);
    assert!(lines[0].contains("Apple"), "expected Apple first in alpha sort, got: {}", out);
    assert!(lines[1].contains("Zebra"), "expected Zebra second in alpha sort, got: {}", out);
}

#[test]
fn test_last_updated_serde_default() {
    // Verify that deserializing an IssueState without last_updated works (backward compat)
    let json = r#"{
        "id": "abc123",
        "title": "Test",
        "body": "",
        "status": "open",
        "close_reason": null,
        "closed_by": null,
        "labels": [],
        "assignees": [],
        "comments": [],
        "created_at": "2025-01-01T00:00:00Z",
        "author": {"name": "Alice", "email": "alice@example.com"}
    }"#;
    let state: IssueState = serde_json::from_str(json).unwrap();
    assert_eq!(state.last_updated, "");
    assert_eq!(state.title, "Test");
}