use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use assert_cmd::prelude::*;
use greentic_pack::reader::{SigningPolicy, open_pack};
use greentic_types::cbor::canonical;
use greentic_types::schemas::common::schema_ir::{AdditionalProperties, SchemaIr};
use greentic_types::schemas::component::v0_6_0::{
ComponentDescribe, ComponentInfo, ComponentOperation, ComponentRunInput, ComponentRunOutput,
schema_hash,
};
use sha2::{Digest, Sha256};
use tempfile::TempDir;
fn write_stub_wasm(path: &Path) {
const STUB: &[u8] = &[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dirs");
}
fs::write(path, STUB).expect("write stub wasm");
}
fn write_describe_sidecar(wasm_path: &Path, component_id: &str) {
let input_schema = SchemaIr::Object {
properties: Default::default(),
required: Vec::new(),
additional: AdditionalProperties::Forbid,
};
let output_schema = SchemaIr::Object {
properties: Default::default(),
required: Vec::new(),
additional: AdditionalProperties::Forbid,
};
let config_schema = SchemaIr::Object {
properties: Default::default(),
required: Vec::new(),
additional: AdditionalProperties::Forbid,
};
let hash = schema_hash(&input_schema, &output_schema, &config_schema).expect("schema hash");
let operation = ComponentOperation {
id: "handle_message".to_string(),
display_name: None,
input: ComponentRunInput {
schema: input_schema,
},
output: ComponentRunOutput {
schema: output_schema,
},
defaults: Default::default(),
redactions: Vec::new(),
constraints: Default::default(),
schema_hash: hash,
};
let describe = ComponentDescribe {
info: ComponentInfo {
id: component_id.to_string(),
version: "0.1.0".to_string(),
role: "tool".to_string(),
display_name: None,
},
provided_capabilities: Vec::new(),
required_capabilities: Vec::new(),
metadata: Default::default(),
operations: vec![operation],
config_schema,
};
let bytes = canonical::to_canonical_cbor_allow_floats(&describe).expect("encode describe");
let describe_path = format!("{}.describe.cbor", wasm_path.display());
fs::write(describe_path, bytes).expect("write describe cache");
}
#[test]
fn open_pack_accepts_packc_gtpack() {
let temp = TempDir::new().expect("temp dir");
let pack_dir = temp.path().join("demo-pack");
let workspace_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf();
fs::create_dir_all(pack_dir.join("flows")).expect("flows dir");
fs::create_dir_all(pack_dir.join("components")).expect("components dir");
let wasm_path = pack_dir.join("components/demo.wasm");
write_stub_wasm(&wasm_path);
write_describe_sidecar(&wasm_path, "demo.component");
let digest = format!(
"sha256:{}",
hex::encode(Sha256::digest(fs::read(&wasm_path).expect("read wasm")))
);
let pack_yaml = r#"pack_id: demo.test
version: 0.1.0
kind: application
publisher: Greentic
components:
- id: "demo.component"
version: "0.1.0"
world: "greentic:component/component@0.5.0"
supports: ["messaging"]
profiles:
default: "stateless"
supported: ["stateless"]
capabilities:
wasi: {}
host: {}
operations:
- name: "handle_message"
input_schema: {}
output_schema: {}
wasm: "components/demo.wasm"
flows:
- id: main
file: flows/main.ygtc
tags: [default]
entrypoints: [main]
"#;
fs::write(pack_dir.join("pack.yaml"), pack_yaml).expect("write pack.yaml");
let flow_yaml = r#"id: main
type: messaging
start: call
nodes:
call:
component.exec:
component: demo.component
operation: handle_message
input:
text: "hi"
routing:
- out: true
"#;
fs::write(pack_dir.join("flows/main.ygtc"), flow_yaml).expect("write flow");
let sidecar = format!(
r#"{{
"schema_version": 1,
"flow": "flows/main.ygtc",
"nodes": {{
"call": {{
"source": {{
"kind": "local",
"path": "../components/demo.wasm",
"digest": "{digest}"
}}
}}
}}
}}"#
);
fs::write(pack_dir.join("flows/main.ygtc.resolve.json"), sidecar).expect("write sidecar");
let summary = format!(
r#"{{
"schema_version": 1,
"flow": "main.ygtc",
"nodes": {{
"call": {{
"component_id": "demo.component",
"source": {{
"kind": "local",
"path": "../components/demo.wasm"
}},
"digest": "{digest}"
}}
}}
}}"#
);
fs::write(
pack_dir.join("flows/main.ygtc.resolve.summary.json"),
summary,
)
.expect("write summary");
let pack_path = temp.path().join("pack.gtpack");
Command::new("cargo")
.current_dir(&workspace_root)
.env("GREENTIC_PACK_USE_DESCRIBE_CACHE", "1")
.args([
"run",
"-p",
"greentic-pack",
"--bin",
"greentic-pack",
"--quiet",
"--",
"build",
"--in",
pack_dir.to_str().unwrap(),
"--allow-pack-schema",
"--gtpack-out",
pack_path.to_str().unwrap(),
"--offline",
])
.assert()
.success();
let load = open_pack(&pack_path, SigningPolicy::DevOk)
.expect("packc-generated gtpack should be accepted");
assert_eq!(load.manifest.meta.pack_id, "demo.test");
assert!(!load.report.signature_ok);
assert!(load.report.sbom_ok);
assert!(load.report.warnings.iter().all(|msg| !msg.contains("sbom")));
assert!(!load.sbom.is_empty());
}