arcature 2026.2.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The binary-subcommand dispatch — runtime-agnostic (AP2.1-10).
//!
//! [`dispatch`] routes a parsed [`Subcommand`](super::Subcommand) to the
//! matching [`Operations`](super::Operations) method. It is a pure async
//! function: it does not build or own a runtime, so a caller on the certified
//! Tokio runtime, a custom runtime, or a test can all drive it. The
//! convenience `super::run` wrapper (macros-gated) reads argv and builds
//! the Tokio runtime for the common app-`main` case.
//!
//! # Honest behavior
//!
//! The dispatch never panics on an operation failure (AGENTS.md §17): it
//! surfaces the typed [`crate::EngineError`] from the operation and the
//! caller decides the exit code. An unknown subcommand is a typed
//! [`super::SubcommandError`] from the parse step, not a silent fallback
//! (a deployment that invokes the wrong subcommand name should fail
//! loudly — AGENTS.md §9, §24).

use super::{Operations, Subcommand};

/// Dispatch a parsed [`Subcommand`] to the matching [`Operations`] method.
///
/// `args` are the trailing argv (after the subcommand selector); they are
/// forwarded to the operation. Public so a caller that manages its own
/// runtime can dispatch a parsed subcommand without the env-reading /
/// runtime-building `super::run` wrapper.
pub async fn dispatch(
    operations: &impl Operations,
    subcommand: Subcommand,
    args: &[String],
) -> crate::Result<()> {
    match subcommand {
        Subcommand::Serve => operations.serve(args).await,
        Subcommand::Migrate => operations.migrate(args).await,
        Subcommand::Queue => operations.queue(args).await,
        Subcommand::Schedule => operations.schedule(args).await,
        Subcommand::Doctor => operations.doctor(args).await,
        Subcommand::About => operations.about(args).await,
    }
}

#[cfg(test)]
mod tests {
    use super::dispatch;
    use crate::cli::{Operations, Subcommand, SubcommandError, parse};
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::{Arc, Mutex};

    /// A test `Operations` that records which method was called (via a
    /// distinct marker value) so the test can prove the dispatch routed to
    /// the right method.
    struct RecordingOps {
        calls: Arc<AtomicUsize>,
    }

    impl Operations for RecordingOps {
        fn serve(
            &self,
            _args: &[String],
        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
        {
            let calls = self.calls.clone();
            Box::pin(async move {
                calls.store(1, Ordering::SeqCst);
                Ok(())
            })
        }
        fn migrate(
            &self,
            _args: &[String],
        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
        {
            let calls = self.calls.clone();
            Box::pin(async move {
                calls.store(2, Ordering::SeqCst);
                Ok(())
            })
        }
        fn queue(
            &self,
            _args: &[String],
        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
        {
            let calls = self.calls.clone();
            Box::pin(async move {
                calls.store(3, Ordering::SeqCst);
                Ok(())
            })
        }
        fn schedule(
            &self,
            _args: &[String],
        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
        {
            let calls = self.calls.clone();
            Box::pin(async move {
                calls.store(4, Ordering::SeqCst);
                Ok(())
            })
        }
        fn doctor(
            &self,
            _args: &[String],
        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
        {
            let calls = self.calls.clone();
            Box::pin(async move {
                calls.store(5, Ordering::SeqCst);
                Ok(())
            })
        }
        fn about(
            &self,
            _args: &[String],
        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
        {
            let calls = self.calls.clone();
            Box::pin(async move {
                calls.store(6, Ordering::SeqCst);
                Ok(())
            })
        }
    }

    #[tokio::test]
    async fn dispatch_routes_each_subcommand_to_the_right_method() {
        for (sub, marker) in [
            (Subcommand::Serve, 1usize),
            (Subcommand::Migrate, 2),
            (Subcommand::Queue, 3),
            (Subcommand::Schedule, 4),
            (Subcommand::Doctor, 5),
            (Subcommand::About, 6),
        ] {
            let calls = Arc::new(AtomicUsize::new(0));
            let ops = RecordingOps {
                calls: calls.clone(),
            };
            dispatch(&ops, sub.clone(), &[]).await.expect("dispatch ok");
            assert_eq!(
                calls.load(Ordering::SeqCst),
                marker,
                "dispatch routed {sub:?} to the wrong operation"
            );
        }
    }

    #[tokio::test]
    async fn dispatch_forwards_trailing_args() {
        struct ArgOps {
            captured: Arc<Mutex<Vec<String>>>,
        }
        impl Operations for ArgOps {
            fn serve(
                &self,
                args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                let captured = self.captured.clone();
                let args = args.to_vec();
                Box::pin(async move {
                    *captured.lock().expect("lock") = args;
                    Ok(())
                })
            }
            fn migrate(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn queue(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn schedule(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn doctor(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn about(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
        }
        let captured = Arc::new(Mutex::new(Vec::new()));
        let ops = ArgOps {
            captured: captured.clone(),
        };
        let args = vec!["3001".to_string(), "--no-warmup".to_string()];
        dispatch(&ops, Subcommand::Serve, &args)
            .await
            .expect("serve ok");
        assert_eq!(
            captured.lock().expect("lock").clone(),
            args,
            "trailing args forwarded to the operation"
        );
    }

    #[tokio::test]
    async fn dispatch_surfaces_operation_errors() {
        struct FailingOps;
        impl Operations for FailingOps {
            fn serve(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Err(crate::EngineError::InvalidPort("not-a-port".to_owned())) })
            }
            fn migrate(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn queue(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn schedule(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn doctor(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
            fn about(
                &self,
                _args: &[String],
            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send + '_>>
            {
                Box::pin(async { Ok(()) })
            }
        }
        let result = dispatch(&FailingOps, Subcommand::Serve, &[]).await;
        assert!(matches!(result, Err(crate::EngineError::InvalidPort(_))));
    }

    #[test]
    fn unknown_subcommand_returns_typed_error() {
        assert_eq!(
            parse(["deploy"]),
            Err(SubcommandError::Unknown {
                name: "deploy".to_owned()
            })
        );
    }
}