ironflow-core 3.13.0

Rust workflow engine with Claude Code native agent support
Documentation
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use reqwest::Client;
use serde_json::json;

use super::*;

struct GitLabIssueOp {
    project_id: u64,
    title: String,
}

impl Operation for GitLabIssueOp {
    fn kind(&self) -> &str {
        "gitlab"
    }

    fn execute<'a>(
        &'a self,
        _ctx: &'a OperationContext,
    ) -> Pin<Box<dyn Future<Output = Result<Value, OperationError>> + Send + 'a>> {
        Box::pin(async move {
            Ok(json!({
                "issue_id": 42,
                "url": "https://gitlab.com/issues/42",
                "project_id": self.project_id,
                "title": self.title
            }))
        })
    }

    fn input(&self) -> Option<Value> {
        Some(json!({
            "project_id": self.project_id,
            "title": self.title
        }))
    }
}

struct NoInputOp;

impl Operation for NoInputOp {
    fn kind(&self) -> &str {
        "noop"
    }

    fn execute<'a>(
        &'a self,
        _ctx: &'a OperationContext,
    ) -> Pin<Box<dyn Future<Output = Result<Value, OperationError>> + Send + 'a>> {
        Box::pin(async { Ok(json!({"status": "ok"})) })
    }
}

struct ErrorOp;

impl Operation for ErrorOp {
    fn kind(&self) -> &str {
        "error_test"
    }

    fn execute<'a>(
        &'a self,
        _ctx: &'a OperationContext,
    ) -> Pin<Box<dyn Future<Output = Result<Value, OperationError>> + Send + 'a>> {
        Box::pin(async {
            Err(OperationError::Http {
                status: Some(500),
                message: "test error".to_string(),
            })
        })
    }
}

fn test_ctx() -> OperationContext {
    OperationContext::new(Arc::new(NoopSecretResolver))
}

#[test]
fn operation_kind_identifies_operation_type() {
    let op = GitLabIssueOp {
        project_id: 123,
        title: "Bug".to_string(),
    };
    assert_eq!(op.kind(), "gitlab");
}

#[test]
fn operation_with_input_provides_structured_logging() {
    let op = GitLabIssueOp {
        project_id: 456,
        title: "Feature request".to_string(),
    };
    let input = op.input();
    assert!(input.is_some());

    let input_value = input.unwrap();
    assert_eq!(input_value["project_id"], 456);
    assert_eq!(input_value["title"], "Feature request");
}

#[test]
fn operation_without_input_returns_none() {
    let op = NoInputOp;
    assert_eq!(op.input(), None);
}

#[tokio::test]
async fn operation_execute_returns_json_output() {
    let ctx = test_ctx();
    let op = GitLabIssueOp {
        project_id: 789,
        title: "Test".to_string(),
    };
    let result = op.execute(&ctx).await;
    assert!(result.is_ok());

    let output = result.unwrap();
    assert_eq!(output["issue_id"], 42);
    assert_eq!(output["project_id"], 789);
    assert_eq!(output["title"], "Test");
}

#[tokio::test]
async fn operation_execute_can_return_error() {
    let ctx = test_ctx();
    let op = ErrorOp;
    let result = op.execute(&ctx).await;
    assert!(result.is_err());
}

#[tokio::test]
async fn noop_secret_resolver_returns_none() {
    let resolver = NoopSecretResolver;
    let result = resolver.get("any_key").await;
    assert!(result.is_ok());
    assert!(result.unwrap().is_none());
}

#[test]
fn operation_context_provides_http_client() {
    let ctx = test_ctx();
    let _client = ctx.http_client();
}

#[test]
fn operation_context_with_custom_client() {
    let client = Client::new();
    let ctx = OperationContext::with_http_client(client, Arc::new(NoopSecretResolver));
    let _client = ctx.http_client();
}

#[tokio::test]
async fn operation_context_secrets_delegates_to_resolver() {
    let ctx = test_ctx();
    let result = ctx.secrets().get("missing").await;
    assert!(result.unwrap().is_none());
}

#[test]
fn secret_value_holds_plaintext() {
    let secret = SecretValue {
        value: "sk-ant-12345".to_string(),
    };
    assert_eq!(secret.value, "sk-ant-12345");
}

#[test]
fn secret_value_debug_redacts_value() {
    let secret = SecretValue {
        value: "super-secret-token".to_string(),
    };
    let debug = format!("{secret:?}");
    assert!(debug.contains("[REDACTED]"));
    assert!(!debug.contains("super-secret-token"));
}

#[test]
fn operation_error_secret_display() {
    let err = OperationError::Secret {
        message: "decryption failed".to_string(),
    };
    assert!(err.to_string().contains("secret error"));
    assert!(err.to_string().contains("decryption failed"));
}