test(wfe-yaml): add deno E2E integration tests
29 tests covering full YAML-to-execution round trips: - Basic deno workflows (arithmetic, string output, inputs, multi-step) - Fetch with wiremock (GET JSON, POST, permission-denied) - Mixed shell + deno workflows (both orderings) - File-based deno steps and module imports - Error propagation with terminate behavior and on_failure hooks - Compiler verification (factories, permissions, timeout, env, modules) - Validation (reject missing config/script, accept valid configs) 162 total deno tests, 326 total workspace tests.
This commit is contained in:
845
wfe-yaml/tests/deno_e2e.rs
Normal file
845
wfe-yaml/tests/deno_e2e.rs
Normal file
@@ -0,0 +1,845 @@
|
||||
#![cfg(feature = "deno")]
|
||||
|
||||
//! End-to-end integration tests for Deno steps in full YAML workflows.
|
||||
//!
|
||||
//! These tests compile YAML, register with the WFE host, run to completion,
|
||||
//! and verify outputs — the same flow a real user would follow.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use wfe::models::WorkflowStatus;
|
||||
use wfe::{WorkflowHostBuilder, run_workflow_sync};
|
||||
use wfe_core::test_support::{
|
||||
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
|
||||
};
|
||||
use wfe_yaml::load_workflow_from_str;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn run_yaml_workflow(yaml: &str) -> wfe::models::WorkflowInstance {
|
||||
run_yaml_workflow_with_data(yaml, serde_json::json!({})).await
|
||||
}
|
||||
|
||||
async fn run_yaml_workflow_with_data(
|
||||
yaml: &str,
|
||||
data: serde_json::Value,
|
||||
) -> wfe::models::WorkflowInstance {
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
|
||||
let persistence = Arc::new(InMemoryPersistenceProvider::new());
|
||||
let lock = Arc::new(InMemoryLockProvider::new());
|
||||
let queue = Arc::new(InMemoryQueueProvider::new());
|
||||
|
||||
let host = WorkflowHostBuilder::new()
|
||||
.use_persistence(persistence as Arc<dyn wfe_core::traits::PersistenceProvider>)
|
||||
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
|
||||
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
for (key, factory) in compiled.step_factories {
|
||||
host.register_step_factory(&key, factory).await;
|
||||
}
|
||||
|
||||
host.register_workflow_definition(compiled.definition.clone())
|
||||
.await;
|
||||
host.start().await.unwrap();
|
||||
|
||||
let instance = run_workflow_sync(
|
||||
&host,
|
||||
&compiled.definition.id,
|
||||
compiled.definition.version,
|
||||
data,
|
||||
Duration::from_secs(15),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
host.stop().await;
|
||||
instance
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 7: Full YAML deno workflow E2E tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_yaml_deno_workflow_basic() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-basic
|
||||
version: 1
|
||||
steps:
|
||||
- name: compute
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
const data = inputs();
|
||||
output("result", (data.x || 10) + (data.y || 20));
|
||||
"#;
|
||||
let instance = run_yaml_workflow_with_data(yaml, serde_json::json!({"x": 3, "y": 7})).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
// The deno step output gets merged into workflow data.
|
||||
let data = instance.data;
|
||||
assert_eq!(data["result"], serde_json::json!(10));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_yaml_deno_workflow_string_output() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-string
|
||||
version: 1
|
||||
steps:
|
||||
- name: greet
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("message", "hello from deno");
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["message"], serde_json::json!("hello from deno"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_fetch_wiremock() {
|
||||
let server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/api/data"))
|
||||
.respond_with(
|
||||
wiremock::ResponseTemplate::new(200)
|
||||
.set_body_json(serde_json::json!({"value": 42})),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let yaml = format!(
|
||||
r#"
|
||||
workflow:
|
||||
id: deno-fetch
|
||||
version: 1
|
||||
steps:
|
||||
- name: fetch-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
const resp = await fetch("{}/api/data");
|
||||
const data = await resp.json();
|
||||
output("fetched_value", data.value);
|
||||
permissions:
|
||||
net:
|
||||
- "127.0.0.1"
|
||||
"#,
|
||||
server.uri()
|
||||
);
|
||||
let instance = run_yaml_workflow(&yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["fetched_value"], serde_json::json!(42));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_fetch_post() {
|
||||
let server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("POST"))
|
||||
.and(wiremock::matchers::path("/api/submit"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(201).set_body_string("accepted"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let yaml = format!(
|
||||
r#"
|
||||
workflow:
|
||||
id: deno-fetch-post
|
||||
version: 1
|
||||
steps:
|
||||
- name: post-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
const resp = await fetch("{}/api/submit", {{
|
||||
method: "POST",
|
||||
headers: {{ "content-type": "application/json" }},
|
||||
body: JSON.stringify({{ key: "val" }})
|
||||
}});
|
||||
output("status", resp.status);
|
||||
output("body", await resp.text());
|
||||
permissions:
|
||||
net:
|
||||
- "127.0.0.1"
|
||||
"#,
|
||||
server.uri()
|
||||
);
|
||||
let instance = run_yaml_workflow(&yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["status"], serde_json::json!(201));
|
||||
assert_eq!(instance.data["body"], serde_json::json!("accepted"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_permissions_enforced() {
|
||||
// Deno step without net permissions should fail when trying to fetch.
|
||||
let server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::any())
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string("ok"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let yaml = format!(
|
||||
r#"
|
||||
workflow:
|
||||
id: deno-perms
|
||||
version: 1
|
||||
steps:
|
||||
- name: denied-fetch
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
try {{
|
||||
await fetch("{}");
|
||||
output("result", "should_not_reach");
|
||||
}} catch (e) {{
|
||||
output("denied", true);
|
||||
output("error_msg", e.message || String(e));
|
||||
}}
|
||||
"#,
|
||||
server.uri()
|
||||
);
|
||||
let instance = run_yaml_workflow(&yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["denied"], serde_json::json!(true));
|
||||
let err_msg = instance.data["error_msg"].as_str().unwrap_or("");
|
||||
assert!(
|
||||
err_msg.contains("Permission denied"),
|
||||
"expected permission denied, got: {err_msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_mixed_shell_and_deno() {
|
||||
let wfe_prefix = "##wfe";
|
||||
let yaml = format!(
|
||||
r#"
|
||||
workflow:
|
||||
id: mixed-wf
|
||||
version: 1
|
||||
steps:
|
||||
- name: shell-step
|
||||
type: shell
|
||||
config:
|
||||
run: echo "{wfe_prefix}[output greeting=hello]"
|
||||
- name: deno-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("computed", 100 + 200);
|
||||
"#
|
||||
);
|
||||
let instance = run_yaml_workflow(&yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
|
||||
// Both steps completed.
|
||||
let complete_count = instance
|
||||
.execution_pointers
|
||||
.iter()
|
||||
.filter(|p| p.status == wfe::models::PointerStatus::Complete)
|
||||
.count();
|
||||
assert!(
|
||||
complete_count >= 2,
|
||||
"Expected at least 2 completed pointers, got {complete_count}"
|
||||
);
|
||||
|
||||
// Deno output should be present.
|
||||
assert_eq!(instance.data["computed"], serde_json::json!(300));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_then_shell() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-then-shell
|
||||
version: 1
|
||||
steps:
|
||||
- name: deno-first
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("from_deno", "js_value");
|
||||
- name: shell-second
|
||||
type: shell
|
||||
config:
|
||||
run: echo done
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["from_deno"], serde_json::json!("js_value"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_multiple_steps() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-multi
|
||||
version: 1
|
||||
steps:
|
||||
- name: step-a
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("a", 1);
|
||||
- name: step-b
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("b", 2);
|
||||
- name: step-c
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("c", 3);
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["a"], serde_json::json!(1));
|
||||
assert_eq!(instance.data["b"], serde_json::json!(2));
|
||||
assert_eq!(instance.data["c"], serde_json::json!(3));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_inputs_from_workflow_data() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-inputs
|
||||
version: 1
|
||||
steps:
|
||||
- name: use-inputs
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
const data = inputs();
|
||||
output("doubled", data.value * 2);
|
||||
"#;
|
||||
let instance =
|
||||
run_yaml_workflow_with_data(yaml, serde_json::json!({"value": 21})).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["doubled"], serde_json::json!(42));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_timeout_in_yaml() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-timeout
|
||||
version: 1
|
||||
steps:
|
||||
- name: quick-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("done", true);
|
||||
timeout: "5s"
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["done"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_file_step() {
|
||||
// Create a temp JS file.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let script_path = dir.path().join("step.js");
|
||||
{
|
||||
let mut f = std::fs::File::create(&script_path).unwrap();
|
||||
writeln!(f, "output('from_file', 'file_value');").unwrap();
|
||||
}
|
||||
|
||||
let yaml = format!(
|
||||
r#"
|
||||
workflow:
|
||||
id: deno-file
|
||||
version: 1
|
||||
steps:
|
||||
- name: file-step
|
||||
type: deno
|
||||
config:
|
||||
file: "{}"
|
||||
"#,
|
||||
script_path.to_str().unwrap()
|
||||
);
|
||||
let instance = run_yaml_workflow(&yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["from_file"], serde_json::json!("file_value"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_modules_import() {
|
||||
// Create a temp helper module and a main script that imports it.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let helper_path = dir.path().join("helper.js");
|
||||
{
|
||||
let mut f = std::fs::File::create(&helper_path).unwrap();
|
||||
writeln!(f, "export function double(x) {{ return x * 2; }}").unwrap();
|
||||
}
|
||||
|
||||
// Write the main script to a file too, since inline YAML with import braces is tricky.
|
||||
let main_path = dir.path().join("main.js");
|
||||
{
|
||||
let main_code = format!(
|
||||
"import {{ double }} from \"file://{}\";\noutput(\"result\", double(21));",
|
||||
helper_path.to_str().unwrap()
|
||||
);
|
||||
std::fs::write(&main_path, &main_code).unwrap();
|
||||
}
|
||||
|
||||
let dir_str = dir.path().to_str().unwrap().to_string();
|
||||
let main_path_str = main_path.to_str().unwrap().to_string();
|
||||
let yaml = format!(
|
||||
r#"
|
||||
workflow:
|
||||
id: deno-module
|
||||
version: 1
|
||||
steps:
|
||||
- name: module-step
|
||||
type: deno
|
||||
config:
|
||||
file: "{main_path_str}"
|
||||
permissions:
|
||||
read:
|
||||
- "{dir_str}"
|
||||
"#
|
||||
);
|
||||
let instance = run_yaml_workflow(&yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["result"], serde_json::json!(42));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compiler / Validation integration verification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn compiler_produces_deno_step_factory() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: compiler-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: js-compute
|
||||
type: deno
|
||||
config:
|
||||
script: "output('x', 1);"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
assert!(!compiled.step_factories.is_empty());
|
||||
let (key, _factory) = &compiled.step_factories[0];
|
||||
assert!(
|
||||
key.contains("deno"),
|
||||
"factory key should contain 'deno', got: {key}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_step_with_permissions() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: perm-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: net-step
|
||||
type: deno
|
||||
config:
|
||||
script: "1;"
|
||||
permissions:
|
||||
net:
|
||||
- "api.example.com"
|
||||
read:
|
||||
- "/tmp"
|
||||
env:
|
||||
- "HOME"
|
||||
dynamic_import: true
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
assert!(!compiled.step_factories.is_empty());
|
||||
// Verify the step config was serialized correctly.
|
||||
let step = compiled
|
||||
.definition
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.name.as_deref() == Some("net-step"))
|
||||
.unwrap();
|
||||
let cfg: serde_json::Value = step.step_config.clone().unwrap();
|
||||
assert_eq!(cfg["permissions"]["net"][0], "api.example.com");
|
||||
assert_eq!(cfg["permissions"]["read"][0], "/tmp");
|
||||
assert_eq!(cfg["permissions"]["env"][0], "HOME");
|
||||
assert_eq!(cfg["permissions"]["dynamic_import"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_step_with_timeout() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: timeout-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: timed
|
||||
type: deno
|
||||
config:
|
||||
script: "1;"
|
||||
timeout: "3s"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
let step = compiled
|
||||
.definition
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.name.as_deref() == Some("timed"))
|
||||
.unwrap();
|
||||
let cfg: serde_json::Value = step.step_config.clone().unwrap();
|
||||
assert_eq!(cfg["timeout_ms"], serde_json::json!(3000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_step_with_file() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: file-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: file-step
|
||||
type: deno
|
||||
config:
|
||||
file: "./scripts/run.js"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
let step = compiled
|
||||
.definition
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.name.as_deref() == Some("file-step"))
|
||||
.unwrap();
|
||||
let cfg: serde_json::Value = step.step_config.clone().unwrap();
|
||||
assert_eq!(cfg["file"], "./scripts/run.js");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_deno_step_no_config() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: bad
|
||||
version: 1
|
||||
steps:
|
||||
- name: no-config
|
||||
type: deno
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let result = load_workflow_from_str(yaml, &config);
|
||||
match result {
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
assert!(
|
||||
msg.contains("config") || msg.contains("Deno"),
|
||||
"got: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("expected error for deno step without config"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_deno_step_no_script_or_file() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: bad
|
||||
version: 1
|
||||
steps:
|
||||
- name: empty-config
|
||||
type: deno
|
||||
config:
|
||||
env:
|
||||
FOO: bar
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let result = load_workflow_from_str(yaml, &config);
|
||||
match result {
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
assert!(
|
||||
msg.contains("script") || msg.contains("file"),
|
||||
"got: {msg}"
|
||||
);
|
||||
}
|
||||
Ok(_) => panic!("expected error for deno step without script or file"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_accepts_deno_step_with_script() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: ok
|
||||
version: 1
|
||||
steps:
|
||||
- name: good
|
||||
type: deno
|
||||
config:
|
||||
script: "1+1;"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
assert!(load_workflow_from_str(yaml, &config).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_accepts_deno_step_with_file() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: ok
|
||||
version: 1
|
||||
steps:
|
||||
- name: good
|
||||
type: deno
|
||||
config:
|
||||
file: "./run.js"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
assert!(load_workflow_from_str(yaml, &config).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_mixed_shell_and_deno_produces_both_factories() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: mixed
|
||||
version: 1
|
||||
steps:
|
||||
- name: shell-step
|
||||
type: shell
|
||||
config:
|
||||
run: echo hi
|
||||
- name: deno-step
|
||||
type: deno
|
||||
config:
|
||||
script: "output('x', 1);"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
let has_shell = compiled.step_factories.iter().any(|(k, _)| k.contains("shell"));
|
||||
let has_deno = compiled.step_factories.iter().any(|(k, _)| k.contains("deno"));
|
||||
assert!(has_shell, "should have shell factory");
|
||||
assert!(has_deno, "should have deno factory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_step_with_modules_list() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: mod-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: with-mods
|
||||
type: deno
|
||||
config:
|
||||
script: "1;"
|
||||
modules:
|
||||
- "npm:lodash@4"
|
||||
- "npm:is-number@7"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
let step = compiled
|
||||
.definition
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.name.as_deref() == Some("with-mods"))
|
||||
.unwrap();
|
||||
let cfg: serde_json::Value = step.step_config.clone().unwrap();
|
||||
let modules = cfg["modules"].as_array().unwrap();
|
||||
assert_eq!(modules.len(), 2);
|
||||
assert_eq!(modules[0], "npm:lodash@4");
|
||||
assert_eq!(modules[1], "npm:is-number@7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_step_with_env() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: env-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: env-step
|
||||
type: deno
|
||||
config:
|
||||
script: "1;"
|
||||
env:
|
||||
FOO: bar
|
||||
BAZ: qux
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
let step = compiled
|
||||
.definition
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.name.as_deref() == Some("env-step"))
|
||||
.unwrap();
|
||||
let cfg: serde_json::Value = step.step_config.clone().unwrap();
|
||||
assert_eq!(cfg["env"]["FOO"], "bar");
|
||||
assert_eq!(cfg["env"]["BAZ"], "qux");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error handling E2E
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_error_propagates() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-error
|
||||
version: 1
|
||||
error_behavior:
|
||||
type: terminate
|
||||
steps:
|
||||
- name: boom
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
throw new Error("kaboom");
|
||||
error_behavior:
|
||||
type: terminate
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
// The workflow should terminate because the step fails with terminate error behavior.
|
||||
assert_eq!(instance.status, WorkflowStatus::Terminated);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_with_on_failure_hook() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-hook
|
||||
version: 1
|
||||
steps:
|
||||
- name: failing-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
throw new Error("intentional");
|
||||
on_failure:
|
||||
name: cleanup
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("cleaned", true);
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
// This should compile without errors.
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
assert!(compiled.step_factories.len() >= 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_log_does_not_crash() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-log
|
||||
version: 1
|
||||
steps:
|
||||
- name: logging-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
log("test message from deno");
|
||||
output("logged", true);
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["logged"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn yaml_deno_complex_json_output() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: deno-json
|
||||
version: 1
|
||||
steps:
|
||||
- name: json-step
|
||||
type: deno
|
||||
config:
|
||||
script: |
|
||||
output("nested", { a: { b: { c: 42 } } });
|
||||
output("array", [1, 2, 3]);
|
||||
output("null_val", null);
|
||||
output("bool_val", false);
|
||||
"#;
|
||||
let instance = run_yaml_workflow(yaml).await;
|
||||
assert_eq!(instance.status, WorkflowStatus::Complete);
|
||||
assert_eq!(instance.data["nested"]["a"]["b"]["c"], serde_json::json!(42));
|
||||
assert_eq!(instance.data["array"], serde_json::json!([1, 2, 3]));
|
||||
assert!(instance.data["null_val"].is_null());
|
||||
assert_eq!(instance.data["bool_val"], serde_json::json!(false));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow description and error_behavior
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_workflow_with_description() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: described
|
||||
version: 2
|
||||
description: "A workflow with deno steps"
|
||||
steps:
|
||||
- name: js
|
||||
type: deno
|
||||
config:
|
||||
script: "1;"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
assert_eq!(
|
||||
compiled.definition.description.as_deref(),
|
||||
Some("A workflow with deno steps")
|
||||
);
|
||||
assert_eq!(compiled.definition.version, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiler_deno_step_with_error_behavior() {
|
||||
let yaml = r#"
|
||||
workflow:
|
||||
id: eb-test
|
||||
version: 1
|
||||
steps:
|
||||
- name: retry-step
|
||||
type: deno
|
||||
config:
|
||||
script: "1;"
|
||||
error_behavior:
|
||||
type: retry
|
||||
max_retries: 5
|
||||
interval: "2s"
|
||||
"#;
|
||||
let config = HashMap::new();
|
||||
let compiled = load_workflow_from_str(yaml, &config).unwrap();
|
||||
let step = compiled
|
||||
.definition
|
||||
.steps
|
||||
.iter()
|
||||
.find(|s| s.name.as_deref() == Some("retry-step"))
|
||||
.unwrap();
|
||||
assert!(step.error_behavior.is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user