#![cfg(feature = "constructors-wasm")]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::time::Duration;
use cloacina::registry::loader::constructor_loader::{load_trigger_constructor, TriggerBinding};
use cloacina::trigger::TriggerResult;
use cloacina_constructor_contract::{PrimitiveKind, ProviderManifest};
use serde::Serialize;
#[derive(Serialize)]
struct Config {
should_fire: bool,
message: String,
}
fn fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/constructor-contract/trigger-constructor-macro-fixture")
}
fn component() -> &'static [u8] {
static BYTES: OnceLock<Vec<u8>> = OnceLock::new();
BYTES.get_or_init(|| {
let fixture = fixture_dir();
let status = Command::new("cargo")
.args(["build", "--lib", "--target", "wasm32-wasip2", "--release"])
.current_dir(&fixture)
.status()
.expect("spawn cargo build --target wasm32-wasip2");
assert!(
status.success(),
"trigger-constructor-macro-fixture wasm build failed"
);
std::fs::read(
fixture.join("target/wasm32-wasip2/release/trigger_constructor_macro_fixture.wasm"),
)
.expect("read built wasm component")
})
}
fn macro_manifest_json() -> &'static str {
static JSON: OnceLock<String> = OnceLock::new();
JSON.get_or_init(|| {
let out = Command::new("cargo")
.args(["run", "--quiet", "--bin", "emit_manifest"])
.current_dir(fixture_dir())
.output()
.expect("spawn cargo run --bin emit_manifest");
assert!(
out.status.success(),
"emit_manifest failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).expect("emit_manifest stdout is UTF-8")
})
}
fn stage(root: &Path) {
let dir = root.join("trigger-constructor-macro-pkg");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("trigger_constructor_macro_fixture.wasm"),
component(),
)
.unwrap();
std::fs::write(
dir.join("package.toml"),
"[package]\nname = \"trigger-constructor-macro-pkg\"\nversion = \"0.1.0\"\n\
interface = \"trigger-constructor\"\ninterface_version = 1\nruntime = \"wasm\"\n\n\
[metadata]\ncategory = \"constructor\"\n\n\
[wasm]\ncomponent = \"trigger_constructor_macro_fixture.wasm\"\n",
)
.unwrap();
std::fs::write(dir.join("provider.json"), macro_manifest_json()).unwrap();
}
#[tokio::test]
async fn macro_authored_trigger_manifest_carries_the_declared_surface() {
let provider = ProviderManifest::from_json(macro_manifest_json())
.expect("macro provider manifest parses against the real contract crate");
let manifest = provider
.constructor("heartbeat")
.expect("suite carries the member");
assert_eq!(manifest.name, "heartbeat");
assert_eq!(manifest.version, "0.1.0");
assert_eq!(manifest.primitive_kind, PrimitiveKind::Trigger);
assert_eq!(manifest.interface, "trigger-constructor");
assert_eq!(manifest.interface_version, 1);
assert!(manifest.params.is_empty());
}
#[tokio::test]
async fn macro_authored_wasm_trigger_constructor_fires_when_configured() {
let tmp = tempfile::TempDir::new().unwrap();
stage(tmp.path());
let trigger = load_trigger_constructor(
tmp.path(),
"trigger-constructor-macro-pkg",
"heartbeat",
&Config {
should_fire: true,
message: "boundary crossed".into(),
},
TriggerBinding {
poll_interval: Duration::from_secs(5),
allow_concurrent: true,
workflow_name: "my_workflow".into(),
cron_expression: None,
},
&cloacina::registry::loader::grants::ResolvedGrants::deny_all(),
)
.expect("load_trigger_constructor");
assert_eq!(trigger.name(), "heartbeat");
assert_eq!(trigger.poll_interval(), Duration::from_secs(5));
let result = trigger.poll().await.expect("poll");
assert!(result.should_fire(), "config should_fire=true → Fire");
let ctx = result.into_context().expect("fire carries a context");
assert_eq!(
ctx.get("reason"),
Some(&serde_json::json!("boundary crossed")),
"the macro-generated poll `set`s the fire context's `reason`"
);
}
#[tokio::test]
async fn macro_authored_wasm_trigger_constructor_skips_when_configured_off() {
let tmp = tempfile::TempDir::new().unwrap();
stage(tmp.path());
let trigger = load_trigger_constructor(
tmp.path(),
"trigger-constructor-macro-pkg",
"heartbeat",
&Config {
should_fire: false,
message: "unused".into(),
},
TriggerBinding::default(),
&cloacina::registry::loader::grants::ResolvedGrants::deny_all(),
)
.expect("load_trigger_constructor");
let result = trigger.poll().await.expect("poll");
assert!(
matches!(result, TriggerResult::Skip),
"config should_fire=false → Skip, got {result:?}"
);
}