malvin 0.2.5

Non-interactive research and coding agent
use crate::session_dotfile_backup::gate_restore_merge::merge_for_gate_restore;
use crate::session_dotfile_backup::{
    DotfileBackupPayload, DotfileBackupState, GitignoreBackup, GitignoreFileBackup,
    SessionDotfileBackups,
};

fn present(bytes: &[u8]) -> DotfileBackupState {
    DotfileBackupState::Present(DotfileBackupPayload {
        backup_path: std::path::PathBuf::from("/tmp/test"),
        bytes: bytes.to_vec(),
    })
}

fn gitignore_present(bytes: &[u8]) -> GitignoreBackup {
    GitignoreBackup::Present {
        backup_root: std::path::PathBuf::from("/tmp/test"),
        files: vec![GitignoreFileBackup {
            rel: std::path::PathBuf::from(".gitignore"),
            bytes: bytes.to_vec(),
        }],
    }
}

fn bundle_with(gitignore: GitignoreBackup, checks: DotfileBackupState) -> SessionDotfileBackups {
    SessionDotfileBackups {
        malvin_checks: checks,
        gitignore,
        vision: crate::session_dotfile_backup::VisionBackup::Missing,
        malvin_config_workspace: DotfileBackupState::Missing,
    }
}

#[test]
fn merge_rejects_deleted_vision() {
    let vision_present = |bytes: &[u8]| crate::session_dotfile_backup::VisionBackup::Present {
        backup_root: std::path::PathBuf::from("/tmp/test"),
        files: vec![crate::session_dotfile_backup::VisionFileBackup {
            rel: std::path::PathBuf::from("VISION.md"),
            bytes: bytes.to_vec(),
        }],
    };
    let mut anchor = bundle_with(GitignoreBackup::Missing, DotfileBackupState::Missing);
    anchor.vision = vision_present(b"baseline\n");
    let mut progress = anchor.clone();
    progress.vision = crate::session_dotfile_backup::VisionBackup::Missing;
    let merged = merge_for_gate_restore(&anchor, &progress);
    assert!(matches!(
        merged.vision,
        crate::session_dotfile_backup::VisionBackup::Present { .. }
    ));
}

#[test]
fn merge_rejects_deleted_gitignore() {
    let anchor = bundle_with(gitignore_present(b"baseline\n"), present(b"make lint\n"));
    let progress = bundle_with(GitignoreBackup::Missing, present(b"make lint\n"));
    let merged = merge_for_gate_restore(&anchor, &progress);
    assert!(matches!(merged.gitignore, GitignoreBackup::Present { .. }));
    assert!(matches!(
        merged.malvin_checks,
        DotfileBackupState::Present(_)
    ));
}

#[test]
fn merge_rejects_tampered_malvin_checks() {
    let anchor = bundle_with(GitignoreBackup::Missing, present(b"make lint\n"));
    let progress = bundle_with(GitignoreBackup::Missing, present(b"TAMPERED\n"));
    let merged = merge_for_gate_restore(&anchor, &progress);
    let DotfileBackupState::Present(ref payload) = merged.malvin_checks else {
        panic!("expected malvin_checks present");
    };
    assert_eq!(payload.bytes, b"make lint\n");
}

#[test]
fn merge_keeps_agent_edited_vision_content() {
    let vision_present = |bytes: &[u8]| crate::session_dotfile_backup::VisionBackup::Present {
        backup_root: std::path::PathBuf::from("/tmp/test"),
        files: vec![crate::session_dotfile_backup::VisionFileBackup {
            rel: std::path::PathBuf::from("VISION.md"),
            bytes: bytes.to_vec(),
        }],
    };
    let mut anchor = bundle_with(GitignoreBackup::Missing, DotfileBackupState::Missing);
    anchor.vision = vision_present(b"baseline\n");
    let mut progress = anchor.clone();
    progress.vision = vision_present(b"improved vision\n");
    let merged = merge_for_gate_restore(&anchor, &progress);
    let crate::session_dotfile_backup::VisionBackup::Present { files, .. } = merged.vision else {
        panic!("expected vision present");
    };
    assert_eq!(files[0].bytes, b"improved vision\n");
}

#[test]
fn merge_keeps_agent_edited_gitignore_content() {
    let anchor = bundle_with(gitignore_present(b"baseline\n"), present(b"make lint\n"));
    let progress = bundle_with(
        gitignore_present(b"baseline\nextra/\n"),
        present(b"make lint\n"),
    );
    let merged = merge_for_gate_restore(&anchor, &progress);
    let GitignoreBackup::Present { files, .. } = merged.gitignore else {
        panic!("expected gitignore present");
    };
    assert_eq!(files[0].bytes, b"baseline\nextra/\n");
}

#[test]
fn merge_keeps_progress_when_gitignore_present_without_root_file() {
    let nested_only = GitignoreBackup::Present {
        backup_root: std::path::PathBuf::from("/tmp/test"),
        files: vec![GitignoreFileBackup {
            rel: std::path::PathBuf::from("pkg/.gitignore"),
            bytes: b"pkg\n".to_vec(),
        }],
    };
    let anchor = bundle_with(GitignoreBackup::Missing, present(b"make lint\n"));
    let progress = bundle_with(nested_only, present(b"make lint\n"));
    let merged = merge_for_gate_restore(&anchor, &progress);
    let GitignoreBackup::Present { files, .. } = merged.gitignore else {
        panic!("expected present");
    };
    assert_eq!(files[0].rel, std::path::PathBuf::from("pkg/.gitignore"));
}

#[test]
fn merge_keeps_progress_when_vision_present_without_root_file() {
    let nested_only = crate::session_dotfile_backup::VisionBackup::Present {
        backup_root: std::path::PathBuf::from("/tmp/test"),
        files: vec![crate::session_dotfile_backup::VisionFileBackup {
            rel: std::path::PathBuf::from("pkg/VISION.md"),
            bytes: b"pkg\n".to_vec(),
        }],
    };
    let mut anchor = bundle_with(GitignoreBackup::Missing, DotfileBackupState::Missing);
    anchor.vision = crate::session_dotfile_backup::VisionBackup::Missing;
    let mut progress = anchor.clone();
    progress.vision = nested_only;
    let merged = merge_for_gate_restore(&anchor, &progress);
    let crate::session_dotfile_backup::VisionBackup::Present { files, .. } = merged.vision else {
        panic!("expected vision present");
    };
    assert_eq!(files[0].rel, std::path::PathBuf::from("pkg/VISION.md"));
}