octorus 0.6.2

A TUI tool for GitHub PR review, designed for Helix editor users
Documentation
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;

const HELP_BANNER_LINE: &str =
    "  ██████╗   ██████╗ ████████╗  ██████╗  ██████╗  ██╗   ██╗ ███████╗";

#[test]
fn help_exits_successfully() {
    cargo_bin_cmd!("or")
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains(HELP_BANNER_LINE));
}

#[test]
fn version_exits_successfully() {
    cargo_bin_cmd!("or")
        .arg("--version")
        .assert()
        .success()
        .stdout(predicate::str::starts_with("or "));
}

#[test]
fn init_help_exits_successfully() {
    cargo_bin_cmd!("or")
        .args(["init", "--help"])
        .assert()
        .success();
}

// No-args launches the Cockpit TUI (alternate screen), so we can't test
// the full flow via assert_cmd. But we CAN verify that the binary does NOT
// fall back to printing help — it should attempt to enter TUI mode and
// eventually fail or hang (timeout), never printing "Usage:" to stdout.
// The ASCII-art banner check is omitted because crossterm may render it
// via escape sequences during TUI init, causing false positives.
#[test]
fn no_args_does_not_print_help() {
    let output = cargo_bin_cmd!("or")
        .timeout(std::time::Duration::from_secs(3))
        .output()
        .expect("failed to execute");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        !stdout.contains("Usage"),
        "no-args should enter Cockpit, not print help"
    );
}

#[test]
fn invalid_repo_exits_with_error() {
    cargo_bin_cmd!("or")
        .args(["--repo", "invalid/nonexistent-repo-12345", "--pr", "1"])
        .assert()
        .failure();
}

#[test]
fn pr_flag_only_enters_pr_list() {
    cargo_bin_cmd!("or")
        .args(["--repo", "invalid/nonexistent-repo-12345", "--pr"])
        .assert()
        .failure()
        .stdout(predicate::str::contains("Usage").not());
}

#[test]
fn pr_short_flag_only_enters_pr_list() {
    cargo_bin_cmd!("or")
        .args(["--repo", "invalid/nonexistent-repo-12345", "-p"])
        .assert()
        .failure()
        .stdout(predicate::str::contains("Usage").not());
}

#[test]
fn issue_flag_only_enters_issue_list() {
    cargo_bin_cmd!("or")
        .args(["--repo", "invalid/nonexistent-repo-12345", "--issue"])
        .assert()
        .failure()
        .stdout(predicate::str::contains("Usage").not());
}

#[test]
fn issue_short_flag_only_enters_issue_list() {
    cargo_bin_cmd!("or")
        .args(["--repo", "invalid/nonexistent-repo-12345", "-i"])
        .assert()
        .failure()
        .stdout(predicate::str::contains("Usage").not());
}

#[test]
fn update_local_comment_missing_id_exits_non_zero() {
    let tmp = tempfile::tempdir().expect("create tempdir");
    cargo_bin_cmd!("or")
        .args([
            "update-local-comment",
            "--repo",
            "owner/repo",
            "--working-dir",
            tmp.path().to_str().unwrap(),
            "--resolve",
            "999",
        ])
        .env("XDG_CACHE_HOME", tmp.path())
        .assert()
        .failure()
        .stdout(predicate::str::contains("Missing IDs: 999"))
        .stderr(predicate::str::contains("unknown local comment ID"));
}

#[test]
fn local_comments_purge_removes_file_and_reports_count() {
    let tmp = tempfile::tempdir().expect("create tempdir");
    let workdir = tmp.path().join("worktree");
    std::fs::create_dir_all(&workdir).unwrap();

    // Seed a comment so purge has something to delete.
    let comments_dir = tmp.path().join("octorus").join("local-comments");
    std::fs::create_dir_all(&comments_dir).unwrap();
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    std::hash::Hash::hash(&workdir.to_string_lossy().as_ref(), &mut hasher);
    let workdir_hash = std::hash::Hasher::finish(&hasher);
    let path = comments_dir.join(format!("owner_repo-{:016x}.json", workdir_hash));
    std::fs::write(
        &path,
        r#"{"version":1,"comments":[{"id":1,"path":"a.rs","line":1,"body":"x","user":{"login":"u"},"created_at":"2026-04-27T00:00:00Z"}]}"#,
    )
    .unwrap();

    cargo_bin_cmd!("or")
        .args([
            "local-comments",
            "--repo",
            "owner/repo",
            "--working-dir",
            workdir.to_str().unwrap(),
            "--purge",
        ])
        .env("XDG_CACHE_HOME", tmp.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("Purged 1 local comment"));

    assert!(!path.exists());
}

#[test]
fn local_comments_purge_with_no_file_reports_zero() {
    let tmp = tempfile::tempdir().expect("create tempdir");
    cargo_bin_cmd!("or")
        .args([
            "local-comments",
            "--repo",
            "owner/repo",
            "--working-dir",
            tmp.path().to_str().unwrap(),
            "--purge",
        ])
        .env("XDG_CACHE_HOME", tmp.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("Purged 0 local comments"));
}