kcode-dev-tools 0.4.3

Session-scoped tools for managed Rust libraries, Web libraries, and Rust binaries
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use kcode_dev_tools::{
    ATTACH_OBJECT_WEB_LIB_TOOL, CALL_RUST_BIN_TOOL, CREATE_RUST_BIN_TOOL, CREATE_RUST_LIB_TOOL,
    CREATE_WEB_LIB_TOOL, Config, DOCS_RUST_BIN_TOOL, DOCS_RUST_LIB_TOOL, DOCS_WEB_LIB_TOOL,
    ManagedSourceKind, Service, WRITE_FILE_FREEFORM_RUST_BIN_TOOL,
    WRITE_FILE_FREEFORM_RUST_LIB_TOOL, WRITE_FILE_FREEFORM_WEB_LIB_TOOL,
};
use serde_json::json;

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

struct TestRoot(PathBuf);

impl TestRoot {
    fn new() -> Self {
        let counter = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "kcode-dev-tools-integration-{}-{counter}",
            std::process::id()
        ));
        fs::create_dir(&path).unwrap();
        Self(path)
    }

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

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

fn service(root: &Path) -> Service {
    Service::open(Config {
        rust_libraries_root: root.join("rust-libs"),
        web_libraries_root: root.join("web-libs"),
        web_publications_root: root.join("web-publications"),
        rust_binaries_root: root.join("rust-bins"),
        rust_binary_publications_root: root.join("rust-bin-publications"),
        crates_io_registry_token: "test-token".to_owned(),
    })
    .unwrap()
}

#[tokio::test]
async fn one_service_manages_all_three_source_kinds_and_releases_together() {
    let root = TestRoot::new();
    let service = service(root.path());
    assert_eq!(
        service.web_publications_root(),
        fs::canonicalize(root.path().join("web-publications")).unwrap()
    );
    let session = "conversation:test";
    for (create, write, docs, name, path, kind) in [
        (
            CREATE_RUST_LIB_TOOL,
            WRITE_FILE_FREEFORM_RUST_LIB_TOOL,
            DOCS_RUST_LIB_TOOL,
            "demo-lib",
            "src/extra.rs",
            ManagedSourceKind::RustLibrary,
        ),
        (
            CREATE_WEB_LIB_TOOL,
            WRITE_FILE_FREEFORM_WEB_LIB_TOOL,
            DOCS_WEB_LIB_TOOL,
            "demo-web",
            "extra.js",
            ManagedSourceKind::WebLibrary,
        ),
        (
            CREATE_RUST_BIN_TOOL,
            WRITE_FILE_FREEFORM_RUST_BIN_TOOL,
            DOCS_RUST_BIN_TOOL,
            "demo-bin",
            "src/extra.rs",
            ManagedSourceKind::RustBinary,
        ),
    ] {
        let created = service
            .execute(session, create, json!({"name":name}), Vec::new())
            .await
            .unwrap();
        assert_eq!(created.snapshot.as_ref().unwrap().kind, kind);

        let written = service
            .execute(
                session,
                write,
                json!({"name":name,"path":path,"contents":"// managed\n"}),
                Vec::new(),
            )
            .await
            .unwrap();
        assert!(written.snapshot.unwrap().text.contains(path));

        let documentation = service
            .execute(session, docs, json!({"name":name}), Vec::new())
            .await
            .unwrap();
        assert!(documentation.text.contains("Version: 0.1.0"));
    }

    assert_eq!(service.release(session).await.unwrap(), 3);
}

#[tokio::test]
async fn object_tools_require_their_matching_resolved_payloads() {
    let root = TestRoot::new();
    let service = service(root.path());
    let mismatch = service
        .execute(
            "call:test",
            CALL_RUST_BIN_TOOL,
            json!({
                "name":"demo",
                "version":"^1",
                "objectIds":["pending:1"],
            }),
            Vec::new(),
        )
        .await
        .unwrap_err();
    assert_eq!(mismatch.code, "invalid_arguments");
    assert_eq!(
        mismatch.message,
        "objectIds and resolved objects must have the same length."
    );

    let wrong_tool = service
        .execute(
            "call:test",
            DOCS_RUST_LIB_TOOL,
            json!({"name":"demo"}),
            vec![vec![1, 2, 3]],
        )
        .await
        .unwrap_err();
    assert_eq!(wrong_tool.code, "invalid_arguments");
    assert_eq!(
        wrong_tool.message,
        "Resolved objects are accepted only by the Web-library attachment and Rust-binary call tools."
    );
}

#[tokio::test]
async fn web_libraries_attach_opaque_objects_at_relative_paths() {
    let root = TestRoot::new();
    let service = service(root.path());
    let session = "assets:test";
    service
        .execute(
            session,
            CREATE_WEB_LIB_TOOL,
            json!({"name":"asset-web"}),
            Vec::new(),
        )
        .await
        .unwrap();

    let bytes = b"<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>".to_vec();
    let attached = service
        .execute(
            session,
            ATTACH_OBJECT_WEB_LIB_TOOL,
            json!({
                "name":"asset-web",
                "path":"assets/images/logo.svg",
                "objectId":"pending:1",
            }),
            vec![bytes.clone()],
        )
        .await
        .unwrap();
    let snapshot = attached.snapshot.unwrap();
    assert!(snapshot.text.contains("Asset: assets/images/logo.svg"));
    assert!(snapshot.text.contains(&format!("Bytes: {}", bytes.len())));
    assert!(!snapshot.text.contains("SHA-256:"));

    let reopened = kcode_web_libs::open(
        root.path().join("web-libs"),
        root.path().join("web-publications"),
        "asset-web",
    )
    .unwrap();
    assert_eq!(reopened.assets[0].bytes, bytes);

    let mismatch = service
        .execute(
            session,
            ATTACH_OBJECT_WEB_LIB_TOOL,
            json!({
                "name":"asset-web",
                "path":"assets/image.png",
                "objectId":"pending:2",
            }),
            Vec::new(),
        )
        .await
        .unwrap_err();
    assert_eq!(
        mismatch.message,
        "objectId requires exactly one resolved object."
    );

    let text_extension = service
        .execute(
            session,
            ATTACH_OBJECT_WEB_LIB_TOOL,
            json!({
                "name":"asset-web",
                "path":"assets/readme.txt",
                "objectId":"pending:3",
            }),
            vec![b"opaque attachment".to_vec()],
        )
        .await
        .unwrap_err();
    assert!(
        text_extension
            .message
            .contains("`assets/readme.txt` has a text extension and must be represented as a File")
    );
}

#[test]
fn web_source_and_publication_roots_must_be_disjoint() {
    let root = TestRoot::new();
    let shared = root.path().join("web");
    let error = match Service::open(Config {
        rust_libraries_root: root.path().join("rust-libs"),
        web_libraries_root: shared.clone(),
        web_publications_root: shared,
        rust_binaries_root: root.path().join("rust-bins"),
        rust_binary_publications_root: root.path().join("rust-bin-publications"),
        crates_io_registry_token: "test-token".to_owned(),
    }) {
        Ok(_) => panic!("overlapping Web roots were accepted"),
        Err(error) => error,
    };
    assert_eq!(error.code, "dev_tools_invalid_root");
    assert_eq!(
        error.message,
        "Managed Web source and publication roots must be disjoint."
    );
}