malvin 0.2.4

Non-interactive research and coding agent
use crate::workspace_paths::{
    MALVIN_DIR, MALVIN_USER_HOME_DIR, canonical_work_dir_for_logs, find_malvin_logs_root,
    git_worktree_toplevel, is_malvin_workspace, legacy_malvin_checks_path,
    malvin_acp_spawn_chamber_dir, malvin_advice_path, malvin_checks_path, malvin_config_path,
    malvin_data_root, malvin_home_config_path, malvin_home_logs_root, malvin_home_snapshots_root,
    malvin_logs_root, malvin_user_home_root, read_work_dir_manifest,
    remove_legacy_malvin_checks_file, resolve_malvin_checks_path, snapshot_category_dir,
    workspace_logs_hash, write_work_dir_manifest,
};

fn path_helpers_and_workspace_marker() {
    let _ = crate::seed_malvin_config;
    crate::test_utils::with_isolated_home(|work| {
        assert_eq!(
            malvin_checks_path(work),
            crate::user_home_dir().join(".malvin").join("gates")
        );
        assert_eq!(malvin_advice_path(work), work.join(".malvin/advice.md"));
        assert_eq!(malvin_config_path(work), malvin_home_config_path());
        assert!(!is_malvin_workspace(work));
        std::fs::create_dir_all(work.join(MALVIN_DIR)).unwrap();
        assert!(is_malvin_workspace(work));
    });
}

fn malvin_data_root_uses_git_toplevel_when_inside_repo() {
    let tmp = tempfile::tempdir().unwrap();
    let repo = tmp.path().join("repo");
    std::fs::create_dir_all(repo.join("src")).unwrap();
    assert!(
        std::process::Command::new("git")
            .args(["init"])
            .current_dir(&repo)
            .status()
            .unwrap()
            .success()
    );
    let sub = repo.join("src");
    let repo = repo.canonicalize().expect("canonicalize");
    assert_eq!(malvin_data_root(&sub), repo);
    assert_eq!(malvin_checks_path(&sub), repo.join(".malvin").join("gates"));
    assert_eq!(
        malvin_acp_spawn_chamber_dir(&sub),
        repo.join(".malvin").join("acp_spawn")
    );
    assert!(git_worktree_toplevel(&sub).is_some());
    assert!(
        crate::workspace_paths::workspace_paths_data_root::git_worktree_toplevel(&sub).is_some()
    );
}

fn resolve_malvin_checks_path_falls_back_to_legacy_cwd_relative() {
    crate::test_utils::with_isolated_home(|work| {
        let legacy = legacy_malvin_checks_path(work);
        std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
        std::fs::write(&legacy, "legacy gate\n").unwrap();
        assert_eq!(resolve_malvin_checks_path(work), legacy);
    });
}

fn workspace_logs_hash_is_stable_hex() {
    let tmp = tempfile::tempdir().unwrap();
    let w = tmp.path().join("proj");
    std::fs::create_dir_all(&w).unwrap();
    let h1 = workspace_logs_hash(&w);
    let h2 = workspace_logs_hash(&w);
    assert_eq!(h1, h2);
    assert_eq!(h1.len(), 16);
    assert!(h1.bytes().all(|b| b.is_ascii_hexdigit()));
}

fn workspace_logs_hash_differs_for_different_paths() {
    let tmp = tempfile::tempdir().unwrap();
    let a = tmp.path().join("a");
    let b = tmp.path().join("b");
    std::fs::create_dir_all(&a).unwrap();
    std::fs::create_dir_all(&b).unwrap();
    assert_ne!(workspace_logs_hash(&a), workspace_logs_hash(&b));
}

fn malvin_user_home_root_uses_malvin_home_dir() {
    let root = malvin_user_home_root();
    assert!(root.ends_with(MALVIN_USER_HOME_DIR));
    let temporary_fallback =
        std::env::temp_dir().join(format!("malvin-test-home-{}", std::process::id()));
    assert!(
        root.starts_with(crate::user_home_dir()) || root.starts_with(temporary_fallback),
        "unexpected Malvin home root: {}",
        root.display()
    );
}

fn malvin_logs_root_lives_under_home_not_workspace() {
    let tmp = tempfile::tempdir().unwrap();
    let w = tmp.path().join("ws");
    std::fs::create_dir_all(&w).unwrap();
    let root = malvin_logs_root(&w);
    assert!(root.starts_with(malvin_home_logs_root()));
    assert!(!root.starts_with(w));
}

fn malvin_snapshots_root_lives_under_home() {
    let root = malvin_home_snapshots_root();
    assert!(root.ends_with(".malvin/snapshots") || root.ends_with(".malvin\\snapshots"));
    assert_eq!(snapshot_category_dir("gitignore"), root.join("gitignore"));
}

fn find_malvin_logs_root_none_until_bucket_exists() {
    let tmp = tempfile::tempdir().unwrap();
    let w = tmp.path().join("fresh");
    std::fs::create_dir_all(&w).unwrap();
    assert_eq!(find_malvin_logs_root(&w), None);
    let bucket = malvin_logs_root(&w);
    std::fs::create_dir_all(&bucket).unwrap();
    assert_eq!(find_malvin_logs_root(&w).as_deref(), Some(bucket.as_path()));
}

fn work_dir_manifest_round_trip() {
    let tmp = tempfile::tempdir().unwrap();
    let ws = tmp.path().join("ws");
    let run = tmp.path().join("run");
    std::fs::create_dir_all(&ws).unwrap();
    std::fs::create_dir_all(&run).unwrap();
    write_work_dir_manifest(&run, &ws).unwrap();
    let read = read_work_dir_manifest(&run).expect("manifest");
    assert_eq!(read, canonical_work_dir_for_logs(&ws));
}

fn remove_legacy_malvin_checks_file_deletes_legacy_not_layout_checks() {
    crate::test_utils::with_isolated_home(|work| {
        assert!(
            std::process::Command::new("git")
                .args(["init"])
                .current_dir(work)
                .status()
                .expect("git init")
                .success()
        );
        std::fs::write(work.join(".malvin_checks"), "legacy\n").unwrap();
        std::fs::create_dir_all(work.join(MALVIN_DIR)).unwrap();
        std::fs::write(malvin_checks_path(work), "current\n").unwrap();
        remove_legacy_malvin_checks_file(work);
        assert!(!work.join(".malvin_checks").exists());
        assert_eq!(
            std::fs::read_to_string(malvin_checks_path(work)).unwrap(),
            "current\n"
        );
    });
}

fn home_malvin_config_write_blocked_without_test_mutation_flag() {
    use crate::malvin_config_file::{open_malvin_config, write_config_value};

    crate::test_utils::with_isolated_home(|work| {
        let cfg = malvin_config_path(work);
        assert!(!cfg.exists());
        open_malvin_config(work).expect("ensure default");
        assert!(cfg.is_file());
        crate::test_utils::revoke_home_malvin_config_mutation_for_test();
        let value: toml::Value = toml::from_str("mem_limit_gb = 99").expect("toml");
        assert!(
            write_config_value(&cfg, &value).is_err(),
            "write must fail without mutation consent"
        );
        crate::test_utils::allow_home_malvin_config_mutation_for_test();
    });
}

#[test]
fn kiss_bundled_workspace_paths_tests() {
    path_helpers_and_workspace_marker();
    malvin_data_root_uses_git_toplevel_when_inside_repo();
    resolve_malvin_checks_path_falls_back_to_legacy_cwd_relative();
    workspace_logs_hash_is_stable_hex();
    workspace_logs_hash_differs_for_different_paths();
    malvin_user_home_root_uses_malvin_home_dir();
    malvin_logs_root_lives_under_home_not_workspace();
    malvin_snapshots_root_lives_under_home();
    find_malvin_logs_root_none_until_bucket_exists();
    work_dir_manifest_round_trip();
    remove_legacy_malvin_checks_file_deletes_legacy_not_layout_checks();
    home_malvin_config_write_blocked_without_test_mutation_flag();
}