#![cfg(all(feature = "postgres", feature = "sqlite"))]
use durare::{
DurableContext, DurableEngine, Error, PostgresProvider, Result, Serializer, SqliteProvider,
WorkflowQueue,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::Duration;
const GOLDEN_OUTPUT_JSON: &str = r#"{"echo_text":"hello-interop","echo_num":42,"echo_dt":"2025-06-15T10:30:00.000Z","items_count":3,"meta_keys":["key1","key2","nested"],"flag":true,"empty":null,"received":{"sender":"test","payload":[1,2,3]}}"#;
const GOLDEN_EVENT_JSON: &str = r#"{"text":"hello-interop","num":42,"flag":true}"#;
const GOLDEN_STREAM_JSON: &str = r#"{"item":"hello-interop"}"#;
const GOLDEN_MESSAGE_JSON: &str = r#"{"sender":"test","payload":[1,2,3]}"#;
#[derive(Deserialize)]
struct CanonicalArgs {
text: String,
num: i64,
dt: String,
items: Vec<String>,
meta: serde_json::Map<String, Value>,
flag: bool,
#[serde(default)]
empty: Option<Value>,
}
#[derive(Serialize, Deserialize)]
struct CanonicalOut {
echo_text: String,
echo_num: i64,
echo_dt: String,
items_count: i64,
meta_keys: Vec<String>,
flag: bool,
empty: Option<Value>,
received: Value,
}
#[durare::workflow(name = "canonicalWorkflow")]
async fn canonical_workflow(ctx: DurableContext, args: CanonicalArgs) -> Result<CanonicalOut> {
ctx.set_event(
"interop_status",
json!({"text": args.text, "num": args.num, "flag": args.flag}),
)
.await?;
ctx.write_stream("interop_stream", json!({"item": args.text}))
.await?;
let received: Value = ctx
.recv("interop_topic", Duration::from_secs(5))
.await?
.expect("the pre-inserted portable message");
let mut meta_keys: Vec<String> = args.meta.keys().cloned().collect();
meta_keys.sort();
Ok(CanonicalOut {
echo_text: args.text.clone(),
echo_num: args.num,
echo_dt: args.dt,
items_count: args.items.len() as i64,
meta_keys,
flag: args.flag,
empty: args.empty,
received,
})
}
fn foreign_input_json() -> String {
r#"{"positionalArgs":[{"text":"hello-interop","num":42,"dt":"2025-06-15T10:30:00.000Z","items":["alpha","beta","gamma"],"meta":{"key1":"value1","key2":99,"nested":{"deep":true}},"flag":true,"empty":null}]}"#.to_string()
}
#[tokio::test]
async fn durare_runs_a_foreign_written_portable_workflow() -> Result<()> {
let mut path = std::env::temp_dir();
path.push(format!("durare-interop-db-{}.db", uuid::Uuid::new_v4()));
let url = format!("sqlite://{}", path.display());
let wf_id = uuid::Uuid::new_v4().to_string();
let provider = SqliteProvider::connect(&url)
.await?
.with_serializer(Serializer::Portable);
let mut engine = DurableEngine::new(Arc::new(provider)).await?;
engine.register_queue(WorkflowQueue::new("interopq"));
let foreign = sqlx::sqlite::SqlitePool::connect(&url).await.unwrap();
let now = chrono::Utc::now().timestamp_millis();
sqlx::query(
"INSERT INTO workflow_status
(workflow_uuid, name, inputs, status, executor_id, application_version,
queue_name, priority, serialization, created_at, updated_at)
VALUES (?, ?, ?, ?, '', '', ?, 0, 'portable_json', ?, ?)",
)
.bind(&wf_id)
.bind("canonicalWorkflow")
.bind(foreign_input_json())
.bind("ENQUEUED")
.bind("interopq")
.bind(now)
.bind(now)
.execute(&foreign)
.await
.unwrap();
sqlx::query(
"INSERT INTO notifications
(message_uuid, destination_uuid, topic, message, serialization, created_at_epoch_ms)
VALUES (?, ?, 'interop_topic', ?, 'portable_json', ?)",
)
.bind(uuid::Uuid::new_v4().to_string())
.bind(&wf_id)
.bind(GOLDEN_MESSAGE_JSON)
.bind(now)
.execute(&foreign)
.await
.unwrap();
engine.launch().await?;
let out: CanonicalOut = engine
.retrieve_workflow::<CanonicalOut>(&wf_id)
.await?
.result()
.await?;
assert_eq!(
out.received,
json!({"sender": "test", "payload": [1, 2, 3]})
);
assert_eq!(out.meta_keys, vec!["key1", "key2", "nested"]);
let output: String =
sqlx::query_scalar("SELECT output FROM workflow_status WHERE workflow_uuid = ?")
.bind(&wf_id)
.fetch_one(&foreign)
.await
.unwrap();
assert_eq!(output, GOLDEN_OUTPUT_JSON);
let event: String = sqlx::query_scalar(
"SELECT value FROM workflow_events WHERE workflow_uuid = ? AND key = 'interop_status'",
)
.bind(&wf_id)
.fetch_one(&foreign)
.await
.unwrap();
assert_eq!(event, GOLDEN_EVENT_JSON);
let stream: String = sqlx::query_scalar(
"SELECT value FROM streams WHERE workflow_uuid = ? AND key = 'interop_stream'",
)
.bind(&wf_id)
.fetch_one(&foreign)
.await
.unwrap();
assert_eq!(stream, GOLDEN_STREAM_JSON);
engine.shutdown(Duration::from_secs(1)).await?;
let _ = std::fs::remove_file(&path);
Ok(())
}
#[tokio::test]
async fn durare_runs_a_foreign_written_portable_workflow_pg() -> Result<()> {
let Ok(url) = std::env::var("DATABASE_URL") else {
eprintln!("skipping pg interop test: DATABASE_URL not set");
return Ok(());
};
let wf_id = uuid::Uuid::new_v4().to_string();
let provider = PostgresProvider::connect(&url)
.await?
.with_serializer(Serializer::Portable);
let mut engine = DurableEngine::new(Arc::new(provider)).await?;
engine.register_queue(WorkflowQueue::new("interopq"));
let foreign = sqlx::postgres::PgPool::connect(&url).await.unwrap();
let now = chrono::Utc::now().timestamp_millis();
sqlx::query(
"INSERT INTO dbos.workflow_status
(workflow_uuid, name, inputs, status, executor_id, application_version,
queue_name, priority, serialization, created_at, updated_at)
VALUES ($1, $2, $3, $4, '', '', $5, 0, 'portable_json', $6, $7)",
)
.bind(&wf_id)
.bind("canonicalWorkflow")
.bind(foreign_input_json())
.bind("ENQUEUED")
.bind("interopq")
.bind(now)
.bind(now)
.execute(&foreign)
.await
.unwrap();
sqlx::query(
"INSERT INTO dbos.notifications
(message_uuid, destination_uuid, topic, message, serialization, created_at_epoch_ms)
VALUES ($1, $2, 'interop_topic', $3, 'portable_json', $4)",
)
.bind(uuid::Uuid::new_v4().to_string())
.bind(&wf_id)
.bind(GOLDEN_MESSAGE_JSON)
.bind(now)
.execute(&foreign)
.await
.unwrap();
engine.launch().await?;
let out: CanonicalOut = engine
.retrieve_workflow::<CanonicalOut>(&wf_id)
.await?
.result()
.await?;
assert_eq!(
out.received,
json!({"sender": "test", "payload": [1, 2, 3]})
);
let output: String =
sqlx::query_scalar("SELECT output FROM dbos.workflow_status WHERE workflow_uuid = $1")
.bind(&wf_id)
.fetch_one(&foreign)
.await
.unwrap();
assert_eq!(output, GOLDEN_OUTPUT_JSON);
let event: String = sqlx::query_scalar(
"SELECT value FROM dbos.workflow_events WHERE workflow_uuid = $1 AND key = 'interop_status'",
)
.bind(&wf_id)
.fetch_one(&foreign)
.await
.unwrap();
assert_eq!(event, GOLDEN_EVENT_JSON);
let stream: String = sqlx::query_scalar(
"SELECT value FROM dbos.streams WHERE workflow_uuid = $1 AND key = 'interop_stream'",
)
.bind(&wf_id)
.fetch_one(&foreign)
.await
.unwrap();
assert_eq!(stream, GOLDEN_STREAM_JSON);
engine.shutdown(Duration::from_secs(1)).await?;
Ok(())
}
#[tokio::test]
async fn durare_reads_a_foreign_failed_workflow() -> Result<()> {
let mut path = std::env::temp_dir();
path.push(format!("durare-interop-err-{}.db", uuid::Uuid::new_v4()));
let url = format!("sqlite://{}", path.display());
let wf_id = uuid::Uuid::new_v4().to_string();
let provider = SqliteProvider::connect(&url)
.await?
.with_serializer(Serializer::Portable);
let engine = DurableEngine::new(Arc::new(provider)).await?;
let foreign = sqlx::sqlite::SqlitePool::connect(&url).await.unwrap();
let now = chrono::Utc::now().timestamp_millis();
let envelope = r#"{"name":"InsufficientFunds","message":"balance too low","code":402,"data":{"available":10}}"#;
sqlx::query(
"INSERT INTO workflow_status
(workflow_uuid, name, inputs, status, error, executor_id, application_version,
serialization, created_at, updated_at)
VALUES (?, ?, ?, 'ERROR', ?, '', '', 'portable_json', ?, ?)",
)
.bind(&wf_id)
.bind("chargeCard")
.bind(r#"{"positionalArgs":[]}"#)
.bind(envelope)
.bind(now)
.bind(now)
.execute(&foreign)
.await
.unwrap();
let status = engine
.retrieve_workflow::<()>(&wf_id)
.await?
.get_status()
.await?;
assert_eq!(status.status, "ERROR");
assert_eq!(status.error.as_deref(), Some("balance too low"));
let info = status.error_info.expect("structured portable error");
assert_eq!(info.name, "InsufficientFunds");
assert_eq!(info.code, Some(json!(402)));
assert_eq!(info.data, Some(json!({"available": 10})));
match engine.retrieve_workflow::<()>(&wf_id).await?.result().await {
Err(Error::Portable(pe)) => {
assert_eq!(pe.name, "InsufficientFunds");
assert_eq!(pe.code, Some(json!(402)));
}
other => panic!("expected Error::Portable, got {other:?}"),
}
let _ = std::fs::remove_file(&path);
Ok(())
}