aion-server 0.31.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! The served assistant surface.
//!
//! Every assertion reads the BODY. The ops-console catch-all answers
//! `200 text/html` for any path a binary does not serve, so a status-only probe
//! cannot tell a build that carries the assistant from one that does not — and
//! these tests exist precisely to tell those two apart.

use std::sync::Arc;

use aion::EngineBuilder;
use aion_integration_acp::catalogue;
use aion_store::{EventStore, InMemoryStore};
use axum::http::StatusCode;
use serde_json::Value;
use tower::ServiceExt;

use super::super::router::workflow_router;
use super::super::test_support::{get_request, read_json, runtime_config, server_state};
use crate::assistant::mcp::{ASSISTANT_MCP_PATH, SESSION_TOKEN_KIND, SESSION_TOOL_NAMES};
use crate::namespace::grants::GRANT_WORDS;
use crate::test_support::{EngineUnderTest, StateUnderTest};
use crate::{
    NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces, config::NamespaceMode,
};

type TestResult = Result<(), Box<dyn std::error::Error>>;

/// State over a fresh engine, held by the caller so the engine is shut down
/// when the test ends. `workflow_router(state.clone())` is the router.
async fn router_state() -> Result<StateUnderTest, Box<dyn std::error::Error>> {
    let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
    let engine = EngineUnderTest::new(Arc::new(
        EngineBuilder::new()
            .stop_drain_timeout(std::time::Duration::from_secs(5))
            .store_arc(store)
            .in_memory_visibility()
            .scheduler_threads(1)
            .build()
            .await?,
    ));
    let resolver = NamespaceResolver::from_parts(
        NamespaceMode::SharedEngine,
        Some(engine.handle()),
        Arc::new(StaticWorkflowNamespaces::default()),
        Arc::new(StaticScheduleNamespaces::default()),
    );
    server_state(engine, resolver, runtime_config()).await
}

/// The description body, proven to have come from THIS route rather than from
/// the SPA catch-all.
async fn descriptor() -> Result<Value, Box<dyn std::error::Error>> {
    let state = router_state().await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant")?)
        .await?;
    assert_eq!(response.status(), StatusCode::OK);
    let content_type = response
        .headers()
        .get(axum::http::header::CONTENT_TYPE)
        .and_then(|value| value.to_str().ok())
        .unwrap_or_default()
        .to_owned();
    assert!(
        content_type.starts_with("application/json"),
        "an absent route answers text/html from the SPA catch-all; got `{content_type}`"
    );
    read_json(response).await
}

/// `GET /assistant` describes the harness catalogue this binary ships, walked
/// from the catalogue itself rather than from a list kept by hand here: a
/// harness this build can launch and the description omits is one an operator
/// can only discover by reading the source.
#[tokio::test]
async fn the_server_describes_every_harness_it_ships() -> TestResult {
    // Vacuity control: an emptied catalogue would satisfy the loop below.
    assert!(
        !catalogue::CATALOGUE.is_empty(),
        "the catalogue is empty, so this cell measures nothing"
    );
    let body = descriptor().await?;
    let harnesses = body["harnesses"]
        .as_array()
        .ok_or("the description must carry a harness list")?;
    assert_eq!(harnesses.len(), catalogue::CATALOGUE.len());
    for entry in catalogue::CATALOGUE {
        let described = harnesses
            .iter()
            .find(|harness| harness["name"] == entry.id)
            .ok_or_else(|| format!("the description must name `{}`: {body}", entry.id))?;
        assert_eq!(described["kind"], "acp");
        assert_eq!(described["launch"], entry.launch());
        // Availability is a VENUE reading taken while the description was
        // built, and the hint is carried EXACTLY when it is needed — a client
        // must never render "install Node.js" beside a running harness.
        assert_eq!(described["available"], entry.available());
        assert_eq!(
            described["install_hint"].is_null(),
            entry.available(),
            "the hint is present exactly when the harness is unavailable: {described}"
        );
    }
    Ok(())
}

/// The tool wiring every session's agent is handed is on the description, by
/// NAME — a route and a credential kind, never a command line.
#[tokio::test]
async fn the_description_carries_the_tool_wiring_a_session_is_handed() -> TestResult {
    let body = descriptor().await?;
    let own = &body["tools"]["assistant"];
    assert_eq!(own["server"], "assistant");
    assert_eq!(own["route"], ASSISTANT_MCP_PATH);
    assert_eq!(own["token"]["kind"], SESSION_TOKEN_KIND);
    assert_eq!(own["token"]["minted_by"], "server");
    assert_eq!(own["token"]["scope"], "session");
    let tools = own["tools"]
        .as_array()
        .ok_or("the assistant tool server must publish its catalogue")?;
    assert_eq!(tools.len(), SESSION_TOOL_NAMES.len());
    for name in SESSION_TOOL_NAMES {
        assert!(
            tools.iter().any(|tool| tool == name),
            "the description must name the tool `{name}`: {body}"
        );
    }
    assert!(
        body["tools"]["aion"].is_boolean(),
        "whether the general MCP endpoint is handed over is a stated fact: {body}"
    );
    Ok(())
}

/// Every grant word this deployment defines is described, with whether the
/// reading caller holds it — walked from the vocabulary, so a word that existed
/// in the grammar and not here would be grantable and undiscoverable.
#[tokio::test]
async fn the_description_carries_every_grant_word_for_this_caller() -> TestResult {
    // Vacuity control, for the same reason as the catalogue loop above.
    assert!(
        !GRANT_WORDS.is_empty(),
        "the grant vocabulary is empty, so this cell measures nothing"
    );
    let body = descriptor().await?;
    let grants = body["grants"]
        .as_array()
        .ok_or("the description must carry the grant rows")?;
    assert_eq!(grants.len(), GRANT_WORDS.len());
    for grant in GRANT_WORDS {
        let described = grants
            .iter()
            .find(|row| row["name"] == grant.word())
            .ok_or_else(|| format!("the description must name `{}`: {body}", grant.word()))?;
        assert_eq!(described["description"], grant.description());
        assert!(
            described["held"].is_boolean(),
            "a grant row states whether this caller holds it: {described}"
        );
    }
    Ok(())
}

/// Session availability is stated, and a refusal comes with the reason — never
/// a bare `false` a console has to invent a sentence for. `default_harness` is
/// a caller's own last pick, and `null` before they have opened anything is a
/// complete answer rather than a server-invented choice.
#[tokio::test]
async fn the_description_states_availability_and_the_callers_last_pick() -> TestResult {
    let body = descriptor().await?;
    let enabled = body["sessions_enabled"]
        .as_bool()
        .ok_or("session availability must be stated")?;
    assert_eq!(
        body["sessions_disabled_reason"].is_null(),
        enabled,
        "a refusal must carry the reason and an availability must not: {body}"
    );
    assert!(
        body["default_harness"].is_null(),
        "a caller who has opened nothing has no last pick: {body}"
    );
    Ok(())
}

/// 🔴 The description says nothing about a workflow, because there is no
/// workflow. The assistant is a server-owned session, and a field here naming a
/// workflow type, a task queue, a document or a catalog residency would be a
/// console reading a shape this server no longer has.
#[tokio::test]
async fn the_description_names_no_workflow_document() -> TestResult {
    let body = descriptor().await?;
    for retired in [
        "workflow_type",
        "task_queue",
        "content_hash",
        "document_filename",
        "document_bytes",
        "input_schema",
        "signals",
        "queries",
        "session",
        "residency",
    ] {
        assert!(
            body.get(retired).is_none(),
            "the description must carry no `{retired}`: {body}"
        );
    }
    Ok(())
}

/// The document route is GONE, not merely unused: a build that still served it
/// would hand out a workflow this server cannot run.
#[tokio::test]
async fn the_document_route_is_not_served() -> TestResult {
    let state = router_state().await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant/document")?)
        .await?;
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
    Ok(())
}

/// The discriminating control: this router really does 404 a path it does not
/// serve, so the 200s above are the routes' own answers rather than a router
/// that answers everything.
#[tokio::test]
async fn a_path_this_router_does_not_serve_is_a_404() -> TestResult {
    let state = router_state().await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant/not-a-real-surface")?)
        .await?;
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
    Ok(())
}