aion-toolchain 0.9.2

Server-side Gleam authoring toolchain: shells out to the gleam binary to compile and type-check workflow source, then packages a verified .aion. Embeds no compiler.
Documentation
use std::path::{Path, PathBuf};

use super::{
    entry_module_source_path, is_confined, is_supported_logical_module, single_entry_module,
    validate_project_root,
};
use crate::error::ToolchainError;

fn temp_root(label: &str) -> Result<PathBuf, std::io::Error> {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |elapsed| elapsed.as_nanos());
    let root = std::env::temp_dir().join(format!("aion-toolchain-{label}-{nanos}"));
    std::fs::create_dir_all(&root)?;
    Ok(root)
}

#[test]
fn supported_logical_modules_enforce_gleam_grammar_and_nesting() {
    for valid in ["hello_world", "demo2", "demo@nested", "demo/nested"] {
        assert!(is_supported_logical_module(valid), "must accept `{valid}`");
    }
    for invalid in [
        "",
        "Demo",
        "bad-name",
        "bad name",
        "../escape",
        "demo@..",
        "/abs",
        "demo$bad",
        "demo\\bad",
        "demo//nested",
        "demo/Upper",
    ] {
        assert!(
            !is_supported_logical_module(invalid),
            "must reject `{invalid}`"
        );
    }
}

#[test]
fn entry_module_path_maps_nested_modules_under_src() -> Result<(), Box<dyn std::error::Error>> {
    let root = Path::new("/work");
    let flat = entry_module_source_path(root, "hello_world")?;
    assert_eq!(flat, PathBuf::from("/work/src/hello_world.gleam"));
    let nested = entry_module_source_path(root, "demo@nested")?;
    assert_eq!(nested, PathBuf::from("/work/src/demo/nested.gleam"));
    let source_style = entry_module_source_path(root, "demo/other")?;
    assert_eq!(source_style, PathBuf::from("/work/src/demo/other.gleam"));
    Ok(())
}

#[test]
fn entry_module_path_rejects_traversal() {
    let root = Path::new("/work");
    let result = entry_module_source_path(root, "../../etc/passwd");
    assert!(matches!(result, Err(ToolchainError::InvalidProject { .. })));
}

#[test]
fn confinement_folds_dotdot_lexically() {
    let base = Path::new("/work/src");
    assert!(is_confined(base, Path::new("/work/src/demo.gleam")));
    assert!(is_confined(base, Path::new("/work/src/demo/nested.gleam")));
    assert!(!is_confined(base, Path::new("/work/other.gleam")));
    assert!(!is_confined(base, Path::new("/work/src/../secret.gleam")));
}

#[test]
fn validate_project_root_requires_both_manifests() -> Result<(), Box<dyn std::error::Error>> {
    let root = temp_root("validate")?;
    let cleanup = || {
        let _ = std::fs::remove_dir_all(&root);
    };

    let missing_gleam = validate_project_root(&root);
    assert!(matches!(
        missing_gleam,
        Err(ToolchainError::InvalidProject { .. })
    ));

    std::fs::write(root.join("gleam.toml"), b"name = \"demo\"\n")?;
    let missing_workflow = validate_project_root(&root);
    assert!(matches!(
        missing_workflow,
        Err(ToolchainError::InvalidProject { .. })
    ));

    std::fs::write(root.join("workflow.toml"), b"[[workflow]]\n")?;
    let ok = validate_project_root(&root);
    cleanup();
    ok?;
    Ok(())
}

#[test]
fn single_entry_module_reads_the_descriptor() -> Result<(), Box<dyn std::error::Error>> {
    let root = temp_root("single-entry")?;
    std::fs::write(
        root.join("workflow.toml"),
        b"[[workflow]]\nentry_module = \"hello_world\"\nentry_function = \"run\"\ntimeout_seconds = 30\ninput_schema = \"schemas/input.json\"\noutput_schema = \"schemas/output.json\"\nactivities = []\n",
    )?;
    let entry = single_entry_module(&root);
    let _ = std::fs::remove_dir_all(&root);
    assert_eq!(entry?, "hello_world");
    Ok(())
}

#[test]
fn many_entry_modules_are_rejected() -> Result<(), Box<dyn std::error::Error>> {
    let root = temp_root("many-entry")?;
    std::fs::write(
        root.join("workflow.toml"),
        b"[[workflow]]\nentry_module = \"a\"\n\n[[workflow]]\nentry_module = \"b\"\n",
    )?;
    let entry = single_entry_module(&root);
    let _ = std::fs::remove_dir_all(&root);
    assert!(matches!(entry, Err(ToolchainError::InvalidProject { .. })));
    Ok(())
}

#[test]
fn zero_entry_modules_are_rejected() -> Result<(), Box<dyn std::error::Error>> {
    let root = temp_root("zero-entry")?;
    std::fs::write(root.join("workflow.toml"), b"# no workflows declared\n")?;
    let entry = single_entry_module(&root);
    let _ = std::fs::remove_dir_all(&root);
    assert!(matches!(entry, Err(ToolchainError::InvalidProject { .. })));
    Ok(())
}