aether-wisp 0.5.2

A terminal UI for AI coding agents via the Agent Client Protocol (ACP)
Documentation
use std::path::PathBuf;

use wisp::git_review::{
    DiffDocument, DiffScope, FileDiff, FileStatus, PatchLineKind, StageState, parse_porcelain_status,
    parse_unified_diff,
};

#[test]
fn acp_texts_normalize_into_canonical_numbered_lines() {
    let file = FileDiff::from_texts("src/lib.rs", "before\nkeep\n", "after\nkeep\nnew\n");

    assert_eq!(file.status, FileStatus::Modified);
    assert_eq!(file.old_path.as_deref(), Some("src/lib.rs"));
    let lines = &file.hunks[0].lines;
    assert_eq!(lines[0].kind, PatchLineKind::HunkHeader);
    assert_eq!(lines[1].old_line_no, Some(1));
    assert_eq!(lines[1].new_line_no, None);
    assert_eq!(lines[1].kind, PatchLineKind::Removed);
    assert_eq!(lines[2].old_line_no, None);
    assert_eq!(lines[2].new_line_no, Some(1));
    assert_eq!(lines[2].kind, PatchLineKind::Added);
    assert_eq!(lines.last().and_then(|line| line.new_line_no), Some(3));
}

#[test]
fn git_output_normalizes_rename_binary_and_untracked_files() {
    let diff = concat!(
        "diff --git a/old.txt b/new.txt\n",
        "similarity index 100%\n",
        "rename from old.txt\n",
        "rename to new.txt\n",
        "diff --git a/image.png b/image.png\n",
        "index 123..456\n",
        "Binary files a/image.png and b/image.png differ\n",
    );
    let document = DiffDocument::from_git_output(
        PathBuf::from("/repo"),
        diff,
        "R  old.txt\0new.txt\0?? notes.txt\0",
        [("notes.txt".to_string(), b"hello\n".to_vec())],
        DiffScope::Both,
    )
    .unwrap();

    assert_eq!(document.files.len(), 3);
    assert_eq!(document.files[0].path, "image.png");
    assert!(document.files[0].binary);
    assert_eq!(document.files[1].status, FileStatus::Renamed);
    assert_eq!(document.files[1].old_path.as_deref(), Some("old.txt"));
    assert_eq!(document.files[1].staged, StageState::Staged);
    assert_eq!(document.files[2].status, FileStatus::Untracked);
    assert_eq!(document.files[2].staged, StageState::Unstaged);
}

#[test]
fn porcelain_status_distinguishes_staged_partial_and_unstaged() {
    let status = parse_porcelain_status("M  staged.rs\0MM partial.rs\0 M unstaged.rs\0");

    assert_eq!(status["staged.rs"], StageState::Staged);
    assert_eq!(status["partial.rs"], StageState::PartiallyStaged);
    assert_eq!(status["unstaged.rs"], StageState::Unstaged);
}

#[test]
fn unified_parser_accepts_single_line_hunk_ranges() {
    let files = parse_unified_diff(concat!(
        "diff --git a/a.txt b/a.txt\n",
        "index 111..222\n",
        "--- a/a.txt\n",
        "+++ b/a.txt\n",
        "@@ -1 +1 @@\n",
        "-old\n",
        "+new\n",
    ))
    .unwrap();

    assert_eq!(files[0].hunks[0].old_count, 1);
    assert_eq!(files[0].hunks[0].new_count, 1);
    assert_eq!(files[0].hunks[0].lines[1].text, "old");
}