kcode-rust-libs 0.1.1

Manage, validate, document, and publish small agent-authored Rust crates
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use kcode_rust_libs::{Error, KcodeRustLibs, RustLibFile, RustLibPath};

static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

struct TestDirectory(PathBuf);

impl TestDirectory {
    fn new(label: &str) -> Self {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "kcode-rust-libs-test-{label}-{}-{timestamp}-{counter}",
            std::process::id()
        ));
        fs::create_dir(&path).unwrap();
        Self(path)
    }

    fn path(&self) -> &Path {
        &self.0
    }
}

impl Drop for TestDirectory {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn rust_libs(test: &TestDirectory) -> KcodeRustLibs {
    KcodeRustLibs::new(test.path().join("rust_libs")).unwrap()
}

const SELF_HOSTED_FILES: &[(&str, &str)] = &[
    ("Cargo.lock", include_str!("../Cargo.lock")),
    ("Cargo.toml", include_str!("../Cargo.toml")),
    ("Containerfile", include_str!("../Containerfile")),
    ("Documentation.md", include_str!("../Documentation.md")),
    ("Specification.md", include_str!("../Specification.md")),
    ("Version.txt", include_str!("../Version.txt")),
    ("src/error.rs", include_str!("../src/error.rs")),
    ("src/files.rs", include_str!("../src/files.rs")),
    ("src/lib.rs", include_str!("../src/lib.rs")),
    ("src/model.rs", include_str!("../src/model.rs")),
    ("src/path.rs", include_str!("../src/path.rs")),
    ("src/repository.rs", include_str!("../src/repository.rs")),
    ("src/sandbox.rs", include_str!("../src/sandbox.rs")),
    ("tests/library.rs", include_str!("library.rs")),
];

#[test]
fn this_crate_is_a_self_hosted_managed_rust_library() {
    let test = TestDirectory::new("self-hosting");
    let rust_libs = rust_libs(&test);
    let root = test.path().join("rust_libs/kcode-rust-libs");

    for &(path, contents) in SELF_HOSTED_FILES {
        let destination = root.join(path);
        fs::create_dir_all(destination.parent().unwrap()).unwrap();
        fs::write(destination, contents).unwrap();
    }

    let mut rust_lib = rust_libs.open_rust_lib("kcode-rust-libs").unwrap();
    assert_eq!(rust_lib.docs().version, env!("CARGO_PKG_VERSION"));
    assert_eq!(rust_lib.files().len(), SELF_HOSTED_FILES.len());
    assert!(rust_lib.files().iter().any(|file| {
        file.path.as_str() == "Cargo.toml" && file.contents.contains("name = \"kcode-rust-libs\"")
    }));

    rust_lib
        .write(&[RustLibFile::new(
            RustLibPath::new("tests/agent-maintenance.txt").unwrap(),
            "written through kcode-rust-libs\n",
        )])
        .unwrap();
    assert!(
        rust_lib
            .files()
            .iter()
            .any(|file| file.path.as_str() == "tests/agent-maintenance.txt")
    );
}

#[test]
fn creates_opens_documents_and_overwrites_complete_files() {
    let test = TestDirectory::new("files");
    let rust_libs = rust_libs(&test);
    let mut rust_lib = rust_libs.create_rust_lib("calculator").unwrap();

    assert_eq!(rust_lib.name(), "calculator");
    assert_eq!(rust_lib.docs().version, "0.1.0");
    assert_eq!(rust_lib.docs().documentation, "");
    assert_eq!(
        rust_lib
            .files()
            .iter()
            .map(|file| file.path.as_str())
            .collect::<Vec<_>>(),
        [
            "Cargo.toml",
            "Documentation.md",
            "Version.txt",
            "src/lib.rs"
        ]
    );

    rust_lib
        .write(&[
            RustLibFile::new(
                RustLibPath::new("Documentation.md").unwrap(),
                "Call `value()` to obtain the answer.\n",
            ),
            RustLibFile::new(
                RustLibPath::new("src/lib.rs").unwrap(),
                "pub fn value() -> u8 { 42 }\n",
            ),
            RustLibFile::new(
                RustLibPath::new("tests/value.rs").unwrap(),
                "#[test]\nfn value_is_42() {}\n",
            ),
        ])
        .unwrap();

    assert_eq!(
        rust_lib.docs().documentation,
        "Call `value()` to obtain the answer.\n"
    );
    let source = rust_lib
        .files()
        .iter()
        .find(|file| file.path.as_str() == "src/lib.rs")
        .unwrap();
    assert_eq!(source.contents, "pub fn value() -> u8 { 42 }\n");

    drop(rust_lib);
    let reopened = rust_libs.open_rust_lib("calculator").unwrap();
    assert_eq!(reopened.files().len(), 5);
    assert_eq!(
        reopened
            .files()
            .iter()
            .map(|file| file.path.as_str())
            .collect::<Vec<_>>(),
        [
            "Cargo.toml",
            "Documentation.md",
            "Version.txt",
            "src/lib.rs",
            "tests/value.rs"
        ]
    );
}

#[test]
fn rust_libs_root_contains_libraries_and_the_backed_up_token() {
    let test = TestDirectory::new("backup");
    let rust_libs = rust_libs(&test);
    rust_libs.create_rust_lib("one").unwrap();
    rust_libs.create_rust_lib("two").unwrap();
    fs::write(
        test.path().join("rust_libs/cargo_registry_token.txt"),
        "secret\n",
    )
    .unwrap();

    let mut entries = fs::read_dir(test.path().join("rust_libs"))
        .unwrap()
        .map(|entry| entry.unwrap().file_name().into_string().unwrap())
        .collect::<Vec<_>>();
    entries.sort();
    assert_eq!(entries, ["cargo_registry_token.txt", "one", "two"]);
    assert!(!test.path().join("rust_libs/target").exists());
}

#[test]
fn rejects_bad_names() {
    let test = TestDirectory::new("validation");
    let rust_libs = rust_libs(&test);
    for name in ["", "../outside", "with/slash", "two words", "_leading"] {
        assert!(matches!(
            rust_libs.create_rust_lib(name),
            Err(Error::InvalidRustLibName(_))
        ));
    }
}

#[test]
fn requires_documentation_and_a_canonical_version() {
    let test = TestDirectory::new("metadata");
    let rust_libs = rust_libs(&test);
    let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();

    let result = rust_lib.write(&[
        RustLibFile::new(RustLibPath::new("src/lib.rs").unwrap(), "changed"),
        RustLibFile::new(RustLibPath::new("Version.txt").unwrap(), "1.0.0-beta.1\n"),
    ]);
    assert!(matches!(result, Err(Error::InvalidVersion(_))));
    assert_eq!(
        fs::read_to_string(test.path().join("rust_libs/demo/src/lib.rs")).unwrap(),
        ""
    );

    fs::remove_file(test.path().join("rust_libs/demo/Documentation.md")).unwrap();
    assert!(matches!(
        rust_lib.write(&[RustLibFile::new(
            RustLibPath::new("src/lib.rs").unwrap(),
            "changed"
        )]),
        Err(Error::MissingRequiredFile("Documentation.md"))
    ));
    assert_eq!(
        fs::read_to_string(test.path().join("rust_libs/demo/src/lib.rs")).unwrap(),
        ""
    );

    drop(rust_lib);
    assert!(matches!(
        rust_libs.open_rust_lib("demo"),
        Err(Error::MissingRequiredFile("Documentation.md"))
    ));
}

#[test]
fn rejects_duplicate_write_paths_before_writing() {
    let test = TestDirectory::new("duplicates");
    let rust_libs = rust_libs(&test);
    let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
    let path = RustLibPath::new("src/lib.rs").unwrap();

    let result = rust_lib.write(&[
        RustLibFile::new(path.clone(), "first"),
        RustLibFile::new(path, "second"),
    ]);
    assert!(matches!(result, Err(Error::DuplicateWritePath(_))));
    assert_eq!(
        fs::read_to_string(test.path().join("rust_libs/demo/src/lib.rs")).unwrap(),
        ""
    );
}

#[test]
fn publish_requires_matching_version_file_and_manifest() {
    let test = TestDirectory::new("publish-version");
    let rust_libs = rust_libs(&test);
    let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
    rust_lib
        .write(&[RustLibFile::new(
            RustLibPath::new("Version.txt").unwrap(),
            "1.0.0\n",
        )])
        .unwrap();

    assert!(matches!(
        rust_lib.publish(),
        Err(Error::VersionMismatch {
            version_file,
            cargo_manifest
        }) if version_file == "1.0.0" && cargo_manifest == "0.1.0"
    ));
}

#[cfg(unix)]
#[test]
fn rejects_symlinks_while_opening_and_writing() {
    use std::os::unix::fs::symlink;

    let test = TestDirectory::new("symlinks");
    let rust_libs = rust_libs(&test);
    let mut rust_lib = rust_libs.create_rust_lib("demo").unwrap();
    let outside = test.path().join("outside");
    fs::create_dir(&outside).unwrap();
    symlink(&outside, test.path().join("rust_libs/demo/link")).unwrap();

    let result = rust_lib.write(&[RustLibFile::new(
        RustLibPath::new("link/file.rs").unwrap(),
        "outside",
    )]);
    assert!(matches!(result, Err(Error::SymlinkNotAllowed(_))));
    assert!(!outside.join("file.rs").exists());

    drop(rust_lib);
    assert!(matches!(
        rust_libs.open_rust_lib("demo"),
        Err(Error::SymlinkNotAllowed(_))
    ));
}