runner-core 0.5.9

Core helpers for Greentic runner: pack resolution, signing, session/state glue, and utilities
Documentation
use std::fs;
use std::io::ErrorKind;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::thread;

use anyhow::{Result, anyhow};
use greentic_pack::builder::{FlowBundle, PACK_VERSION, PackBuilder, PackMeta};
use runner_core::packs::PackDigest;
use runner_core::{Index, IndexLocation, PackConfig, PackManager, PackSource};
use semver::Version;
use serde_json::json;
use tiny_http::{Response, Server};

fn sample_meta() -> PackMeta {
    PackMeta {
        pack_version: PACK_VERSION,
        pack_id: "ai.greentic.runner.tests".into(),
        version: Version::parse("0.1.0").unwrap(),
        name: "Test Runner".into(),
        kind: None,
        description: None,
        authors: vec!["Greentic".into()],
        license: None,
        homepage: None,
        support: None,
        vendor: None,
        imports: vec![],
        entry_flows: vec!["qa".into()],
        created_at_utc: "2025-01-01T00:00:00Z".into(),
        events: None,
        repo: None,
        messaging: None,
        interfaces: Vec::new(),
        annotations: serde_json::Map::new(),
        distribution: None,
        components: Vec::new(),
    }
}

fn sample_flow() -> FlowBundle {
    let flow_json = json!({
        "id": "qa",
        "kind": "flow/v1",
        "entry": "start",
        "nodes": []
    });
    let hash = blake3::hash(&serde_json::to_vec(&flow_json).unwrap())
        .to_hex()
        .to_string();
    FlowBundle {
        id: "qa".into(),
        kind: "flow/v1".into(),
        entry: "start".into(),
        yaml: "id: qa\nentry: start\n".into(),
        json: flow_json,
        hash_blake3: hash,
        nodes: Vec::new(),
    }
}

fn build_test_pack(dir: &Path) -> Result<PathBuf> {
    let wasm_path = dir.join("component.wasm");
    fs::write(&wasm_path, b"\0asm\x01\0\0\0")?;
    let out_path = dir.join("sample.gtpack");
    PackBuilder::new(sample_meta())
        .with_flow(sample_flow())
        .with_component_wasm(
            "demo.component",
            Version::parse("1.0.0")?,
            wasm_path.as_path(),
        )
        .build(&out_path)?;
    Ok(out_path)
}

fn compute_digest(path: &Path) -> Result<PackDigest> {
    let bytes = fs::read(path)?;
    Ok(PackDigest::sha256_from_bytes(&bytes))
}

fn write_index(path: &Path, locator: &str, digest: &PackDigest) -> Result<()> {
    let index = json!({
        "demo": {
            "main_pack": {
                "name": "runner.demo",
                "version": "0.1.0",
                "locator": locator,
                "digest": digest.as_str(),
            },
            "overlays": []
        }
    });
    fs::write(path, serde_json::to_vec_pretty(&index)?)?;
    Ok(())
}

fn build_config(index: &Path, cache_dir: &Path, source: PackSource) -> PackConfig {
    PackConfig {
        source,
        index_location: IndexLocation::File(index.to_path_buf()),
        cache_dir: cache_dir.to_path_buf(),
        public_key: None,
        network: None,
    }
}

#[test]
fn resolves_fs_pack() -> Result<()> {
    let temp = tempfile::tempdir()?;
    let pack_path = build_test_pack(temp.path())?;
    let digest = compute_digest(&pack_path)?;
    let index_path = temp.path().join("index.json");
    write_index(&index_path, pack_path.to_str().unwrap(), &digest)?;

    let config = build_config(&index_path, &temp.path().join("cache"), PackSource::Fs);
    let index = Index::load(&config.index_location)?;
    let manager = PackManager::new(config)?;
    let resolved = manager.resolve_all_for_index(&index)?;
    let tenant = resolved.tenants().get("demo").expect("tenant missing");
    assert_eq!(tenant.overlays.len(), 0);
    assert_eq!(
        tenant.main.manifest.meta.pack_id,
        "ai.greentic.runner.tests"
    );
    assert_eq!(tenant.main.digest.as_str(), digest.as_str());
    Ok(())
}

#[test]
fn resolves_http_pack() -> Result<()> {
    let temp = tempfile::tempdir()?;
    let pack_path = build_test_pack(temp.path())?;
    let digest = compute_digest(&pack_path)?;
    let bytes = fs::read(&pack_path)?;

    let listener = match TcpListener::bind("127.0.0.1:0") {
        Ok(listener) => listener,
        Err(err) if err.kind() == ErrorKind::PermissionDenied => {
            eprintln!("skipping http resolver test: {err}");
            return Ok(());
        }
        Err(err) => return Err(err.into()),
    };
    let addr = listener.local_addr()?;
    let server =
        Server::from_listener(listener, None).map_err(|err| anyhow!("server error: {err}"))?;
    thread::spawn(move || {
        if let Ok(request) = server.recv() {
            let response = Response::from_data(bytes.clone());
            let _ = request.respond(response);
        }
    });

    let locator = format!("http://{}/pack.gtpack", addr);
    let index_path = temp.path().join("index.json");
    write_index(&index_path, &locator, &digest)?;
    let config = build_config(&index_path, &temp.path().join("cache"), PackSource::Http);
    let index = Index::load(&config.index_location)?;
    let manager = PackManager::new(config)?;
    let resolved = manager.resolve_all_for_index(&index)?;
    let tenant = resolved.tenants().get("demo").expect("tenant missing");
    assert_eq!(tenant.main.digest.as_str(), digest.as_str());
    Ok(())
}