soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! Test doubles for ring extensions.
//!
//! Gated behind the `test-support` cargo feature. Provides `StubThrough` —
//! a configurable `ThroughRing` implementation that eliminates organ
//! dependencies from platform-layer tests.
//!
//! ## Dev-deps-only restriction
//!
//! Per OPUS §13.2, dev scaffolding is implementation-variable. This module
//! is the only §13.2 surface inside soma-som-core: it is structurally needed
//! for ring-test ergonomics but MUST NOT be importable from any production
//! code path. The `#[cfg(feature = "test-support")]` gate in `lib.rs` +
//! the `[dev-dependencies] features = ["test-support"]` pattern in
//! consumer crates jointly enforce this:
//!
//! - `cargo check` (no `--all-features`) → this module is not compiled
//! - `cargo check --all-features` → this module compiles, used only by
//!   test targets
//! - `[dependencies] features = ["test-support"]` in any consumer crate
//!   is a violation
//!
//! ## Design rationale
//!
//! Platform tests should verify dispatch routing, auth gates, and Tree
//! threading — not organ persistence. `StubThrough` returns canned Trees
//! so tests exercise the ring contract (Tree-in → Tree-out) without
//! reaching into organ stores.

use crate::extension::{DelegationError, Extension, ThroughRing, ViewRing};
use crate::quad::Tree;

/// A configurable `ThroughRing` test double.
///
/// Handles commands matching any of its prefixes and delegates to a
/// caller-provided handler function. No organ dependency needed.
///
/// # Examples
///
/// ```ignore
/// // Echo stub — returns success with command type echoed
/// let stub = StubThrough::echo("test-director", &["user.", "config."]);
///
/// // Custom handler — full control over responses
/// let stub = StubThrough::new("test-director", &["user."], |cmd, _input| {
///     let mut t = Tree::new();
///     t.insert("result.status".into(), b"success".to_vec());
///     t.insert("result.command_type".into(), cmd.as_bytes().to_vec());
///     Ok(t)
/// });
/// ```
pub struct StubThrough {
    stub_name: String,
    prefixes: Vec<String>,
    #[allow(clippy::type_complexity)]
    handler: Box<dyn Fn(&str, &Tree) -> Result<Tree, DelegationError> + Send + Sync>,
}

impl StubThrough {
    /// Creates a stub with a custom dispatch handler.
    ///
    /// The handler receives `(command_type, full_input_tree)` and returns
    /// a result Tree (or `DelegationError` for framework-level failures).
    pub fn new<F>(name: &str, prefixes: &[&str], handler: F) -> Self
    where
        F: Fn(&str, &Tree) -> Result<Tree, DelegationError> + Send + Sync + 'static,
    {
        Self {
            stub_name: name.to_string(),
            prefixes: prefixes.iter().map(|s| s.to_string()).collect(),
            handler: Box::new(handler),
        }
    }

    /// Creates a stub that always returns a success Tree with the command
    /// type and request ID echoed back.
    pub fn echo(name: &str, prefixes: &[&str]) -> Self {
        Self::new(name, prefixes, |cmd_type, input| {
            let mut result = Tree::new();
            result.insert("result.status".into(), b"success".to_vec());
            result.insert("result.command_type".into(), cmd_type.as_bytes().to_vec());
            if let Some(payload) = input.get("command.payload") {
                result.insert("result.payload".into(), payload.clone());
            }
            if let Some(req_id) = input.get("command.request_id") {
                result.insert("result.request_id".into(), req_id.clone());
            }
            Ok(result)
        })
    }

    /// Creates a stub that always returns an error with the given message.
    pub fn failing(name: &str, prefixes: &[&str], error_msg: &str) -> Self {
        let msg = error_msg.to_string();
        Self::new(name, prefixes, move |cmd_type, input| {
            let mut result = Tree::new();
            result.insert("result.status".into(), b"error".to_vec());
            result.insert("result.command_type".into(), cmd_type.as_bytes().to_vec());
            result.insert("result.error".into(), msg.as_bytes().to_vec());
            if let Some(req_id) = input.get("command.request_id") {
                result.insert("result.request_id".into(), req_id.clone());
            }
            Ok(result)
        })
    }
}

impl Extension for StubThrough {
    fn name(&self) -> &str {
        &self.stub_name
    }

}

impl ThroughRing for StubThrough {
    fn handles(&self, command_type: &str) -> bool {
        self.prefixes.iter().any(|p| command_type.starts_with(p))
    }

    fn claimed_prefixes(&self) -> Vec<String> {
        self.prefixes.iter().map(|s| s.to_string()).collect()
    }

    fn dispatch(&self, input: &Tree) -> Result<Tree, DelegationError> {
        let cmd_type = input
            .get("command.type")
            .map(|v| String::from_utf8_lossy(v).into_owned())
            .unwrap_or_default();
        (self.handler)(&cmd_type, input)
    }
}

/// A configurable `ViewRing` test double.
///
/// Parallels `StubThrough` but for view intents. Handles view ids matching
/// any of its prefixes and delegates to a caller-provided handler.
pub struct StubView {
    stub_name: String,
    prefixes: Vec<String>,
    #[allow(clippy::type_complexity)]
    handler: Box<dyn Fn(&str, &Tree) -> Result<Tree, DelegationError> + Send + Sync>,
}

impl StubView {
    /// Creates a stub with a custom projection handler.
    ///
    /// The handler receives `(view_id, full_input_tree)` and returns a
    /// result Tree (or `DelegationError` for framework-level failures).
    pub fn new<F>(name: &str, prefixes: &[&str], handler: F) -> Self
    where
        F: Fn(&str, &Tree) -> Result<Tree, DelegationError> + Send + Sync + 'static,
    {
        Self {
            stub_name: name.to_string(),
            prefixes: prefixes.iter().map(|s| s.to_string()).collect(),
            handler: Box::new(handler),
        }
    }

    /// Creates a stub that always returns success with an empty JSON payload
    /// and the view_id + request_id echoed back under the `view.result.*`
    /// namespace.
    pub fn echo(name: &str, prefixes: &[&str]) -> Self {
        Self::new(name, prefixes, |view_id, input| {
            let mut result = Tree::new();
            result.insert("view.result.status".into(), b"success".to_vec());
            result.insert("view.result.view_id".into(), view_id.as_bytes().to_vec());
            result.insert("view.result.payload".into(), b"{}".to_vec());
            if let Some(req_id) = input.get("view.request_id") {
                result.insert("view.result.request_id".into(), req_id.clone());
            }
            Ok(result)
        })
    }
}

impl Extension for StubView {
    fn name(&self) -> &str {
        &self.stub_name
    }

}

impl ViewRing for StubView {
    fn handles_view(&self, view_id: &str) -> bool {
        self.prefixes.iter().any(|p| view_id == p.as_str() || view_id.starts_with(p))
    }

    fn claimed_view_prefixes(&self) -> Vec<String> {
        self.prefixes.clone()
    }

    fn project(&self, input: &Tree) -> Result<Tree, DelegationError> {
        let view_id = input
            .get("view.id")
            .map(|v| String::from_utf8_lossy(v).into_owned())
            .unwrap_or_default();
        (self.handler)(&view_id, input)
    }
}

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn echo_returns_success() {
        let stub = StubThrough::echo("test", &["user."]);
        let mut input = Tree::new();
        input.insert("command.type".into(), b"user.list".to_vec());
        input.insert("command.request_id".into(), b"req-1".to_vec());

        let result = stub.dispatch(&input).unwrap();
        assert_eq!(result.get("result.status"), Some(&b"success".to_vec()));
        assert_eq!(
            result.get("result.command_type"),
            Some(&b"user.list".to_vec())
        );
        assert_eq!(result.get("result.request_id"), Some(&b"req-1".to_vec()));
    }

    #[test]
    fn handles_matches_prefix() {
        let stub = StubThrough::echo("test", &["user.", "config."]);
        assert!(stub.handles("user.create"));
        assert!(stub.handles("config.set"));
        assert!(!stub.handles("widget.explode"));
    }

    #[test]
    fn stub_view_echo_returns_success() {
        let stub = StubView::echo("test-view", &["organ.mirror"]);
        let mut input = Tree::new();
        input.insert("view.id".into(), b"organ.mirror".to_vec());
        input.insert("view.request_id".into(), b"req-v1".to_vec());

        let result = stub.project(&input).unwrap();
        assert_eq!(result.get("view.result.status"), Some(&b"success".to_vec()));
        assert_eq!(
            result.get("view.result.view_id"),
            Some(&b"organ.mirror".to_vec())
        );
        assert_eq!(result.get("view.result.request_id"), Some(&b"req-v1".to_vec()));
    }

    #[test]
    fn stub_view_handles_exact_and_prefix() {
        let stub = StubView::echo("test", &["organ.mirror", "term.mu."]);
        assert!(stub.handles_view("organ.mirror"));
        assert!(stub.handles_view("term.mu.health"));
        assert!(!stub.handles_view("organ.guard"));
        assert!(!stub.handles_view("term.fu.data"));
    }

    #[test]
    fn failing_returns_error() {
        let stub = StubThrough::failing("test", &["user."], "not found");
        let mut input = Tree::new();
        input.insert("command.type".into(), b"user.get".to_vec());
        input.insert("command.request_id".into(), b"req-2".to_vec());

        let result = stub.dispatch(&input).unwrap();
        assert_eq!(result.get("result.status"), Some(&b"error".to_vec()));
        assert_eq!(result.get("result.error"), Some(&b"not found".to_vec()));
    }

    #[test]
    fn custom_handler_receives_input() {
        let stub = StubThrough::new("test", &["user."], |cmd, input| {
            let mut t = Tree::new();
            t.insert("result.status".into(), b"success".to_vec());
            t.insert("result.command_type".into(), cmd.as_bytes().to_vec());
            // Echo a specific input key to prove we received the full tree
            if let Some(v) = input.get("command.payload") {
                t.insert("result.payload".into(), v.clone());
            }
            Ok(t)
        });

        let mut input = Tree::new();
        input.insert("command.type".into(), b"user.create".to_vec());
        input.insert("command.payload".into(), b"test-data".to_vec());

        let result = stub.dispatch(&input).unwrap();
        assert_eq!(result.get("result.payload"), Some(&b"test-data".to_vec()));
    }
}