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");
}