liminal-server 0.28.0

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-015 R2: on an open exchange every construction act is the declared
//! holder's own, applied through the outright path, with nothing in Lys.

use std::os::unix::fs::PermissionsExt;
use std::sync::Arc;

use liminal::channel::ChannelSupervisor;
use serde_json::json;

use super::{DeclaredActs, NEEDS_A_NAME, OpenSiteEffects};
use crate::agents::config::AgentsConfig;
use crate::agents::open_session::OpenSession;
use crate::server::connection::services::SiteDraftHolder;
use crate::services::lifecycle::plan::StepStatus;
use crate::services::lifecycle::site::test_support::{
    Issuer, TestResult, limits, open_store, spec,
};
use crate::services::lifecycle::site::{SiteChange, SiteError};
use crate::services::lifecycle::templates::TemplatePolicy;
use crate::services::lifecycle::{Lifecycle, ServiceState};

type Failure = Box<dyn std::error::Error>;

/// An open exchange's construction over a real store and agent registry,
/// as `install_open_construction` binds it, with a template root holding
/// one executable. Each test shuts its scheduler down at its end.
struct Open {
    directory: tempfile::TempDir,
    root: tempfile::TempDir,
    lifecycle: Arc<Lifecycle>,
    agents: Arc<crate::agents::runtime::Agents>,
    scheduler: ChannelSupervisor,
}

impl Open {
    fn new() -> Result<Self, Failure> {
        let directory = tempfile::tempdir()?;
        let root = tempfile::tempdir()?;
        let program = root.path().join("echo");
        std::fs::write(&program, b"#!/bin/sh\nexit 0\n")?;
        std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o700))?;
        let store = open_store(&directory.path().join("store"))?;
        let scheduler = ChannelSupervisor::new()?;
        let agents = crate::agents::runtime::Agents::open(
            store.clone(),
            AgentsConfig::default(),
            Lifecycle::OPEN_NETWORK.into(),
            scheduler.clone(),
            "fixture",
            None,
            None,
        )?;
        let lifecycle = Lifecycle::open_bounded(
            &[],
            directory.path().join("services"),
            Lifecycle::OPEN_NETWORK.into(),
            None,
            &limits()?,
        )?;
        lifecycle.configure_sites(
            limits()?,
            store,
            Some(policy(root.path())?),
            Arc::new(OpenSiteEffects::new(Arc::clone(&agents))),
            retained(&scheduler),
        )?;
        Ok(Self {
            directory,
            root,
            lifecycle,
            agents,
            scheduler,
        })
    }

    fn act(&self, holder: &str, operation: &str, target: &str, change: SiteChange) -> Answer {
        DeclaredActs.hold(
            &self.lifecycle,
            &OpenSession::new(holder.into()),
            operation,
            target,
            change,
            10,
        )
    }
}

type Answer = Result<serde_json::Value, SiteError>;

fn policy(root: &std::path::Path) -> Result<TemplatePolicy, serde_json::Error> {
    serde_json::from_value(json!({"roots":[root],"command_bytes":1_048_576,"records":8,
        "arguments":32,"environment_names":16,"text_bytes":1024,"stop_timeout_ms":2000}))
}

fn retained(scheduler: &ChannelSupervisor) -> Arc<crate::agents::retained::Retained> {
    crate::agents::retained::Retained::new(
        AgentsConfig {
            max_event_bytes: 65536,
            ..AgentsConfig::default()
        },
        scheduler.clone(),
        "fixture",
    )
}

fn executed(answer: &serde_json::Value, operation: &str) {
    assert_eq!(
        answer,
        &json!({"state":"executed","grant":"open","approved_by":"open:tom",
            "responsible":"open:tom","operation":operation})
    );
}

#[test]
fn access015_an_open_holder_walks_create_site_through_complete_as_its_own_act() -> TestResult {
    let open = Open::new()?;
    executed(
        &open.act("tom", "op-create", "echo", SiteChange::CreateSite(spec()))?,
        "op-create",
    );
    assert!(
        open.agents
            .registry()
            .place_own("builder", "open:builder", 1)?
    );
    let bind = SiteChange::BindBuilder {
        agent: "builder".into(),
    };
    executed(&open.act("tom", "op-bind", "echo", bind)?, "op-bind");
    open.lifecycle
        .plan_set("echo", "builder", &["build the echo".into()], 20)?;
    open.lifecycle
        .plan_step("echo", "builder", 1, StepStatus::Done, None, 21)?;
    let proposal = open.lifecycle.propose_template(
        "echo",
        &open.root.path().join("echo"),
        Vec::new(),
        Vec::new(),
        1000,
    )?;
    let name = proposal.name.clone();
    let template = SiteChange::CreateTemplate(proposal);
    executed(
        &open.act("tom", "op-template", &name, template)?,
        "op-template",
    );
    executed(
        &open.act("tom", "op-complete", "echo", SiteChange::Complete)?,
        "op-complete",
    );
    let record = open.lifecycle.record("echo")?;
    assert_eq!(record.approved_by, vec!["open:tom".to_owned()]);
    assert_eq!(record.responsible.as_deref(), Some("open:tom"));
    assert_eq!(record.created_by, "open:tom");
    assert!(!record.placed);
    assert_eq!(record.builder.as_deref(), Some("builder"));
    let grants = record.builder_grants.as_ref().ok_or("no builder grants")?;
    assert_eq!(
        (grants.plan.as_str(), grants.serve.as_str()),
        ("open", "open")
    );
    assert_eq!(record.template.as_deref(), Some(name.as_str()));
    assert_eq!(record.state, ServiceState::Asleep);
    assert_eq!(record.approvals[0].draft, "open:tom:op-create");
    let Open {
        directory,
        root,
        lifecycle,
        agents,
        scheduler,
    } = open;
    drop(lifecycle);
    drop(agents);
    let reopened = Lifecycle::open_bounded(
        &[],
        directory.path().join("services"),
        Lifecycle::OPEN_NETWORK.into(),
        None,
        &limits()?,
    )?;
    reopened.configure_sites(
        limits()?,
        open_store(&directory.path().join("store"))?,
        Some(policy(root.path())?),
        Arc::new(Issuer::default()),
        retained(&scheduler),
    )?;
    assert_eq!(reopened.record("echo")?, record);
    scheduler.shutdown();
    Ok(())
}

#[test]
fn access015_the_anonymous_seat_acts_for_no_one_and_a_builder_needs_a_station() -> TestResult {
    let open = Open::new()?;
    let refused = open
        .act("anonymous", "op-1", "echo", SiteChange::CreateSite(spec()))
        .err()
        .ok_or("the anonymous seat created a site")?;
    assert_eq!(refused.to_string(), NEEDS_A_NAME);
    assert!(open.lifecycle.record("echo").is_err());
    open.act("tom", "op-2", "echo", SiteChange::CreateSite(spec()))?;
    let bind = SiteChange::BindBuilder {
        agent: "stranger".into(),
    };
    let refused = open
        .act("tom", "op-3", "echo", bind)
        .err()
        .ok_or("a builder with no station was bound")?;
    assert_eq!(
        refused.to_string(),
        "builder stranger has no station on this exchange; its seat listens to its inbox \
         once to place it"
    );
    assert!(!open.lifecycle.record("echo")?.builder_bound);
    open.scheduler.shutdown();
    Ok(())
}

fn registration() -> liminal::protocol::WorkerRegistration {
    liminal::protocol::WorkerRegistration {
        namespaces: vec!["echo.request".into()],
        task_queue: "echo.request".into(),
        node: None,
        activity_types: vec!["echo.request".into()],
        identity: "builder".into(),
        activities: vec![liminal::protocol::WorkerActivityDescriptor {
            name: "echo.request".into(),
            input_schema_json: "{\"type\":\"object\"}".into(),
            output_schema_json: "{\"type\":\"object\"}".into(),
        }],
    }
}

#[test]
fn access015_an_open_builder_registers_its_trial_and_the_answerable_holder_calls_it() -> TestResult
{
    let open = Open::new()?;
    open.act("tom", "op-1", "echo", SiteChange::CreateSite(spec()))?;
    open.agents
        .registry()
        .place_own("builder", "open:builder", 1)?;
    let bind = SiteChange::BindBuilder {
        agent: "builder".into(),
    };
    open.act("tom", "op-2", "echo", bind)?;
    let other = OpenSession::new("other".into());
    let refused = open
        .lifecycle
        .prepare_trial(7, Some(&other), &registration(), None, 30)
        .err()
        .ok_or("another open seat registered the trial")?;
    assert_eq!(
        refused.to_string(),
        "pass holder other is not the builder of echo"
    );
    let builder = OpenSession::new("builder".into());
    let permit = open
        .lifecycle
        .prepare_trial(8, Some(&builder), &registration(), None, 30)?;
    assert!(permit.is_some());
    let tom = OpenSession::new("tom".into());
    assert!(
        open.lifecycle
            .admit_trial_call(Some(&tom), "echo.request", 30)?
    );
    let refused = open
        .lifecycle
        .admit_trial_call(Some(&other), "echo.request", 30)
        .err()
        .ok_or("a holder not answerable called the trial")?;
    assert_eq!(refused.to_string(), "service echo is a trial");
    drop(permit);
    open.scheduler.shutdown();
    Ok(())
}

#[test]
fn access015_only_a_named_open_seat_or_an_agent_builds_and_answers_as_records_name_it() {
    use super::{answerable, builds};
    let named = OpenSession::new("tom".into());
    let anonymous = OpenSession::new("anonymous".into());
    assert!(builds(&named) && !builds(&anonymous));
    assert_eq!(answerable(&named).as_deref(), Some("open:tom"));
    assert_eq!(answerable(&anonymous), None);
}

#[test]
fn access015_a_gated_installation_with_no_placer_still_refuses_to_create_a_site() -> TestResult {
    let directory = tempfile::tempdir()?;
    let scheduler = ChannelSupervisor::new()?;
    let lifecycle = Lifecycle::open_bounded(
        &[],
        directory.path().join("services"),
        "net".into(),
        None,
        &limits()?,
    )?;
    lifecycle.configure_sites(
        limits()?,
        open_store(&directory.path().join("store"))?,
        None,
        Arc::new(Issuer::default()),
        retained(&scheduler),
    )?;
    let act = crate::services::lifecycle::site::SiteAct {
        target: "echo".into(),
        holder: "person-owner".into(),
        responsible: "person-owner".into(),
        change: SiteChange::CreateSite(spec()),
    };
    let refused = lifecycle
        .apply_outright(&act, "grant-1", "op-1", "words", 10)
        .err()
        .ok_or("a gated site was created with no placer")?;
    assert_eq!(
        refused.to_string(),
        crate::services::lifecycle::LifecycleError::NoPlacer.to_string()
    );
    assert!(lifecycle.record("echo").is_err());
    scheduler.shutdown();
    Ok(())
}