malvin 0.2.4

Non-interactive research and coding agent
#![cfg_attr(test, allow(unsafe_code))]

use super::*;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::{TempDir, tempdir};

fn write_file(path: &Path, bytes: &[u8]) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(path, bytes).unwrap();
}

fn cursor_ready_tree(root: &Path) -> PathBuf {
    let src = root.join(BRIDGES[0].dir_name);
    let marker = BRIDGES[0].package_marker;
    write_file(&src.join("package.json"), b"{}");
    write_file(&src.join("node_modules").join(marker), b"{}");
    write_file(&src.join("dist").join("bridge.js"), b"1");
    src
}

fn bridges_declare_cursor_marker() {
    assert_eq!(BRIDGES.len(), 1);
    assert_eq!(BRIDGES[0].package_marker, "@cursor/sdk/package.json");
    assert!(BRIDGES[0].min_node >= (22, 13));
}

fn fnv1a64_is_stable() {
    assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325);
    assert_ne!(fnv1a64(b"a"), fnv1a64(b"b"));
    assert_eq!(fnv1a64(b"lock"), fnv1a64(b"lock"));
}

fn parse_node_version_accepts_v_prefix() {
    assert_eq!(npm::parse_node_version("v22.13.0"), Some((22, 13)));
    assert_eq!(npm::parse_node_version("22.8.1"), Some((22, 8)));
    assert_eq!(npm::parse_node_version("nope"), None);
}

fn which_finds_sh_on_unix() {
    let _g = crate::test_utils::test_env_lock();
    assert!(npm::which("sh").is_some());
    assert!(npm::which("definitely-not-a-real-bin-xyz").is_none());
}

fn sdk_share_dir_uses_home() {
    let _g = crate::test_utils::test_env_lock();
    let tmp = tempdir().unwrap();
    crate::acp::with_env("HOME", Some(tmp.path().to_str().unwrap()), || {
        assert_eq!(
            sdk_share_dir(),
            tmp.path().join(".malvin_home").join("sdk-bridges")
        );
    });
}

fn copy_dir_recursive_copies_nested_files() {
    let tmp = tempdir().unwrap();
    let from = tmp.path().join("from");
    let to = tmp.path().join("to");
    write_file(&from.join("a.txt"), b"a");
    write_file(&from.join("sub").join("b.txt"), b"b");
    copy::copy_dir_recursive(&from, &to).unwrap();
    assert_eq!(fs::read(to.join("a.txt")).unwrap(), b"a");
    assert_eq!(fs::read(to.join("sub").join("b.txt")).unwrap(), b"b");
}

fn sync_fixture_with_dist(tmp: &TempDir) -> (PathBuf, PathBuf) {
    let src = tmp.path().join("src_bridge");
    let dest = tmp.path().join("dest_bridge");
    write_file(&src.join("package.json"), b"{}");
    write_file(&src.join("package-lock.json"), b"{}");
    write_file(&src.join("dist").join("bridge.js"), b"ok");
    (src, dest)
}

fn sync_bridge_payload_copies_dist_when_present() {
    let tmp = tempdir().unwrap();
    let (src, dest) = sync_fixture_with_dist(&tmp);
    sync::sync_bridge_payload(&src, &dest, "src_bridge");
    assert_eq!(
        fs::read(dest.join("dist").join("bridge.js")).unwrap(),
        b"ok"
    );
}

fn sync_fixture_sources(tmp: &TempDir) -> (PathBuf, PathBuf) {
    let src = tmp.path().join("src_bridge");
    let dest = tmp.path().join("dest_bridge");
    write_file(&src.join("package.json"), b"{}");
    write_file(&src.join("package-lock.json"), b"{}");
    write_file(&src.join("tsconfig.json"), b"{}");
    write_file(&src.join("src").join("bridge.ts"), b"x");
    (src, dest)
}

fn sync_bridge_payload_copies_sources_without_dist() {
    let tmp = tempdir().unwrap();
    let (src, dest) = sync_fixture_sources(&tmp);
    sync::sync_bridge_payload(&src, &dest, "src_bridge");
    assert!(dest.join("src").join("bridge.ts").is_file());
    assert!(dest.join("tsconfig.json").is_file());
}

fn in_tree_ready_requires_marker_and_dist() {
    let tmp = tempdir().unwrap();
    let src = tmp.path().join("cursor-sdk-bridge");
    let bridge = &BRIDGES[0];
    assert!(!in_tree_bridge_ready(&src, bridge));
    write_file(&src.join("node_modules").join(bridge.package_marker), b"{}");
    assert!(!in_tree_bridge_ready(&src, bridge));
    write_file(&src.join("dist").join("bridge.js"), b"1");
    assert!(in_tree_bridge_ready(&src, bridge));
}

fn share_fixture(tmp: &TempDir) -> (PathBuf, PathBuf, &'static [u8]) {
    let src = tmp.path().join("src");
    let dest = tmp.path().join("dest");
    let lock = b"{\"lock\":1}";
    write_file(&src.join("package-lock.json"), lock);
    let bridge = &BRIDGES[0];
    write_file(
        &dest.join("node_modules").join(bridge.package_marker),
        b"{}",
    );
    write_file(&dest.join("dist").join("bridge.js"), b"1");
    (src, dest, lock)
}

fn share_bridge_ready_checks_stamp() {
    let tmp = tempdir().unwrap();
    let (src, dest, lock) = share_fixture(&tmp);
    let bridge = &BRIDGES[0];
    assert!(!share_bridge_ready(&src, &dest, bridge));
    let stamp = format!("{:x}", fnv1a64(lock));
    write_file(
        &dest.join(".malvin-npm-stamp"),
        format!("{stamp}\n").as_bytes(),
    );
    assert!(share_bridge_ready(&src, &dest, bridge));
}

fn ensure_bridge_reuses_in_tree_ready_tree() {
    let tmp = tempdir().unwrap();
    let _ = cursor_ready_tree(tmp.path());
    ensure_bridge(tmp.path(), &BRIDGES[0]);
}

fn run_build_script_skips_when_env_set() {
    let _g = crate::test_utils::test_env_lock();
    crate::acp::with_env("MALVIN_SKIP_SDK_BRIDGES", Some("1"), run_build_script);
}

fn run_build_script_skips_on_docs_rs() {
    let _g = crate::test_utils::test_env_lock();
    crate::acp::with_env("DOCS_RS", Some("1"), || {
        crate::acp::with_env("MALVIN_SKIP_SDK_BRIDGES", None, run_build_script);
    });
}

fn write_stamp_and_verify_install_round_trip() {
    let tmp = tempdir().unwrap();
    let dest = tmp.path().join("dest");
    let bridge = &BRIDGES[0];
    write_file(&dest.join("package-lock.json"), b"abc");
    write_file(
        &dest.join("node_modules").join(bridge.package_marker),
        b"{}",
    );
    write_file(&dest.join("dist").join("bridge.js"), b"1");
    verify_install(&dest, bridge);
    write_stamp(&dest);
    assert!(dest.join(".malvin-npm-stamp").is_file());
}

fn resolve_npm_prefers_npm_env() {
    let _g = crate::test_utils::test_env_lock();
    let tmp = tempdir().unwrap();
    let fake = tmp.path().join("npm");
    write_file(&fake, b"#!/bin/sh\n");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&fake).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&fake, perms).unwrap();
    }
    crate::acp::with_env("npm", Some(fake.to_str().unwrap()), || {
        assert_eq!(npm::resolve_npm(), fake);
    });
}

fn check_node_version_accepts_current_node() {
    let _g = crate::test_utils::test_env_lock();
    if npm::which("node").is_none() {
        return;
    }
    check_node_version(&BRIDGES[0]);
}

#[test]
fn kiss_bundled_sdk_bridge_build_tests() {
    bridges_declare_cursor_marker();
    fnv1a64_is_stable();
    parse_node_version_accepts_v_prefix();
    which_finds_sh_on_unix();
    sdk_share_dir_uses_home();
    copy_dir_recursive_copies_nested_files();
    sync_bridge_payload_copies_dist_when_present();
    sync_bridge_payload_copies_sources_without_dist();
    in_tree_ready_requires_marker_and_dist();
    share_bridge_ready_checks_stamp();
    ensure_bridge_reuses_in_tree_ready_tree();
    run_build_script_skips_when_env_set();
    run_build_script_skips_on_docs_rs();
    write_stamp_and_verify_install_round_trip();
    resolve_npm_prefers_npm_env();
    check_node_version_accepts_current_node();
}