pi_agent_rust 0.8.0

Native AI coding agent CLI - Rust port of Pi Agent
use super::*;

fn cargo(source: Option<&str>, name: &str, version: &str) -> String {
    let source = source.map_or_else(String::new, |source| format!("source = {source:?}\n"));
    format!("[[package]]\nname = {name:?}\nversion = {version:?}\n{source}")
}

#[test]
fn cargo_inventory_does_not_send_private_git_or_workspace_identities() {
    let text = format!(
        "version = 4\n{}{}{}{}",
        cargo(
            Some("registry+https://github.com/rust-lang/crates.io-index"),
            "public-crate",
            "1.2.3"
        ),
        cargo(None, "private-workspace", "0.1.0"),
        cargo(
            Some("git+https://token-secret@example.invalid/private#deadbeef"),
            "private-git",
            "1.0.0"
        ),
        cargo(
            Some("registry+https://example.invalid/index"),
            "private-registry",
            "1.0.0"
        )
    );
    let mut builder = Builder::default();
    parse_cargo(&text, "Cargo.lock", &mut builder).unwrap();
    let inventory = builder.finish(vec![]);
    assert_eq!(inventory.packages.len(), 1);
    assert_eq!(inventory.packages[0].name, "public-crate");
    assert_eq!(inventory.packages[0].ecosystem, "crates.io");
    assert_eq!(inventory.excluded.len(), 3);
    let rendered = serde_json::to_string(&inventory).unwrap();
    for private in [
        "token-secret",
        "private-workspace",
        "private-git",
        "private-registry",
        "example.invalid",
    ] {
        assert!(!rendered.contains(private));
    }
}

#[test]
fn identical_queries_are_deduplicated_without_losing_lockfile_locations() {
    let mut builder = Builder::default();
    for path in ["Cargo.lock", "nested/Cargo.lock", "Cargo.lock"] {
        for version in ["1.2.3", "2.0.0-beta.1+build"] {
            parse_cargo(
                &cargo(
                    Some("sparse+https://index.crates.io/"),
                    "some_crate",
                    version,
                ),
                path,
                &mut builder,
            )
            .unwrap();
        }
    }
    let inventory = builder.finish(vec![]);
    assert_eq!(inventory.packages.len(), 2);
    assert_eq!(inventory.packages[0].locations.len(), 2);
    assert_eq!(inventory.packages[1].locations.len(), 2);
}

#[test]
fn npm_scopes_aliases_and_nested_installs_keep_actual_package_identity() {
    for version in [2, 3] {
        let mut builder = Builder::default();
        let text = serde_json::json!({"lockfileVersion":version,"packages":{
            "":{"name":"workspace","version":"1.0.0"},
            "node_modules/alias":{"name":"real-package","version":"1.2.3","resolved":"https://registry.npmjs.org/real-package/-/real-package-1.2.3.tgz"},
            "node_modules/parent/node_modules/real-package":{"version":"1.2.3","resolved":"https://registry.npmjs.org/real-package/-/real-package-1.2.3.tgz"},
            "node_modules/@scope/pkg":{"version":"2.0.0","resolved":"https://registry.npmjs.org/@scope/pkg/-/pkg-2.0.0.tgz","dev":true}
        }}).to_string();
        parse_npm(&text, "package-lock.json", &mut builder).unwrap();
        let inventory = builder.finish(vec![]);
        assert_eq!(inventory.packages.len(), 2);
        assert!(inventory.excluded.is_empty());
        let alias = inventory
            .packages
            .iter()
            .find(|package| package.name == "real-package")
            .unwrap();
        assert_eq!(alias.locations.len(), 2);
        assert!(
            inventory
                .packages
                .iter()
                .any(|package| package.name == "@scope/pkg")
        );
    }
}

#[test]
fn npm_unknown_or_nonpublic_origins_are_excluded_without_inference() {
    let mut builder = Builder::default();
    let text = serde_json::json!({"lockfileVersion":3,"packages":{
        "node_modules/private":{"version":"1.0.0","resolved":"https://user:password@example.invalid/private.tgz"},
        "node_modules/unknown":{"version":"1.0.0"},
        "node_modules/git":{"version":"1.0.0","resolved":"git+https://github.com/example/repo#sha"},
        "node_modules/link":{"link":true,"resolved":"packages/local"},
        "packages/local":{"name":"local","version":"1.0.0"},
        "node_modules/evil":{"version":"1.0.0","resolved":"https://registry.npmjs.org.evil.invalid/a"}
    }}).to_string();
    parse_npm(&text, "package-lock.json", &mut builder).unwrap();
    let inventory = builder.finish(vec![]);
    assert!(inventory.packages.is_empty());
    assert_eq!(inventory.excluded.len(), 6);
    assert!(
        !serde_json::to_string(&inventory)
            .unwrap()
            .contains("password")
    );
}

#[test]
fn malformed_or_unknown_lockfile_formats_never_become_empty_success() {
    for text in [
        "",
        "version = 999\npackage = []",
        "[[package]]\nname='missing-version'",
    ] {
        assert!(parse_cargo(text, "Cargo.lock", &mut Builder::default()).is_err());
    }
    for text in [
        "{",
        "{}",
        r#"{"lockfileVersion":1,"dependencies":{}}"#,
        r#"{"lockfileVersion":4,"packages":{}}"#,
        r#"{"lockfileVersion":3,"packages":[]}"#,
    ] {
        assert!(parse_npm(text, "package-lock.json", &mut Builder::default()).is_err());
    }
}

#[test]
fn unpinned_versions_are_exclusions_not_fuzzy_osv_queries() {
    let mut builder = Builder::default();
    for version in [
        "^1.0.0",
        "latest",
        "file:private",
        "1.2",
        "git+https://example.invalid",
    ] {
        builder
            .add(
                "npm",
                "package",
                version,
                Location {
                    lockfile: "package-lock.json".into(),
                    package_path: None,
                },
            )
            .unwrap();
    }
    assert!(builder.packages.is_empty());
    assert_eq!(builder.excluded.len(), 5);
}

#[test]
fn selected_paths_are_normalized_but_cannot_escape_or_select_arbitrary_files() {
    assert_eq!(
        normalized_path("./nested//Cargo.lock").unwrap(),
        "nested/Cargo.lock"
    );
    for path in [
        "",
        ".",
        "../Cargo.lock",
        "/tmp/Cargo.lock",
        "a/../Cargo.lock",
        "C:\\Cargo.lock",
        "a\\Cargo.lock",
        "secret.txt",
        "\0Cargo.lock",
        "bad\n/Cargo.lock",
    ] {
        assert!(normalized_path(path).is_err(), "{path:?}");
    }
}

#[test]
fn entry_and_unique_package_bounds_fail_explicitly() {
    let mut builder = Builder {
        entries: MAX_ENTRIES,
        ..Builder::default()
    };
    assert!(builder.entry().is_err());
    for index in 0..MAX_PACKAGES {
        builder.packages.insert(
            ("npm".into(), format!("pkg-{index}"), "1.0.0".into()),
            BTreeSet::new(),
        );
    }
    assert!(
        builder
            .add(
                "npm",
                "one-more",
                "1.0.0",
                Location {
                    lockfile: "package-lock.json".into(),
                    package_path: None
                }
            )
            .is_err()
    );
}

#[cfg(all(unix, not(any(target_os = "espidf", target_os = "redox"))))]
mod filesystem {
    use super::*;

    #[test]
    fn actual_inventory_deduplicates_paths_and_hashes_the_bytes_it_reads() {
        let dir = tempfile::tempdir().unwrap();
        let text = cargo(
            Some("registry+https://github.com/rust-lang/crates.io-index"),
            "example",
            "1.0.0",
        );
        std::fs::write(dir.path().join("Cargo.lock"), &text).unwrap();
        let inventory =
            inventory(dir.path(), &["Cargo.lock".into(), "./Cargo.lock".into()]).unwrap();
        assert_eq!(inventory.lockfiles.len(), 1);
        assert_eq!(inventory.lockfiles[0].sha256, digest(text.as_bytes()));
        assert_eq!(inventory.packages.len(), 1);
        assert_eq!(inventory.packages[0].locations[0].lockfile, "Cargo.lock");
    }

    #[test]
    fn missing_default_or_explicit_lockfiles_do_not_report_clean() {
        let dir = tempfile::tempdir().unwrap();
        assert!(inventory(dir.path(), &[]).is_err());
        std::fs::write(
            dir.path().join("Cargo.lock"),
            cargo(None, "workspace", "1.0.0"),
        )
        .unwrap();
        assert_eq!(inventory(dir.path(), &[]).unwrap().excluded.len(), 1);
        assert!(inventory(dir.path(), &["absent/Cargo.lock".into()]).is_err());
    }

    #[test]
    fn leaf_and_ancestor_symlinks_are_rejected() {
        use std::os::unix::fs::symlink;
        let dir = tempfile::tempdir().unwrap();
        let outside = tempfile::tempdir().unwrap();
        std::fs::write(
            outside.path().join("Cargo.lock"),
            cargo(None, "private", "1.0.0"),
        )
        .unwrap();
        symlink(
            outside.path().join("Cargo.lock"),
            dir.path().join("Cargo.lock"),
        )
        .unwrap();
        assert!(inventory(dir.path(), &[]).is_err());
        symlink(outside.path(), dir.path().join("nested")).unwrap();
        assert!(inventory(dir.path(), &["nested/Cargo.lock".into()]).is_err());
    }

    #[test]
    fn oversized_regular_lockfile_is_rejected_before_reading() {
        let dir = tempfile::tempdir().unwrap();
        let file = std::fs::File::create(dir.path().join("Cargo.lock")).unwrap();
        file.set_len(MAX_FILE_BYTES + 1).unwrap();
        assert!(
            inventory(dir.path(), &[])
                .unwrap_err()
                .to_string()
                .contains("4 MiB")
        );
    }
}