use std::collections::BTreeMap;
use std::sync::Arc;
use aion::EngineBuilder;
use aion_core::AssistantSessionId;
use aion_store::{EventStore, InMemoryStore, StoreError};
use axum::http::StatusCode;
use serde_json::Value;
use tower::ServiceExt;
use super::super::router::workflow_router;
use super::super::test_support::{
get_request, json_request, read_json, runtime_config, server_state,
};
use super::session_refusal;
use aion_integration_acp::catalogue;
use crate::assistant::sessions::AssistantSessionError;
use crate::config::{
AssistantConfig, AssistantHarnessConfig, NamespaceMode, ResolvedAssistantConfig,
};
use crate::namespace::grants::GRANT_WORDS;
use crate::test_support::{EngineUnderTest, StateUnderTest};
use crate::{NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces};
type TestResult = Result<(), Box<dyn std::error::Error>>;
async fn assistant_state(
assistant: ResolvedAssistantConfig,
) -> 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()),
);
let mut config = runtime_config();
config.auth.enabled = false;
config.assistant = assistant;
server_state(engine, resolver, config).await
}
fn stock_assistant() -> ResolvedAssistantConfig {
ResolvedAssistantConfig::default()
}
fn assistant_with_an_account() -> ResolvedAssistantConfig {
AssistantConfig {
harnesses: vec![AssistantHarnessConfig {
name: Some(CATALOGUE_HARNESS.to_owned()),
accounts: vec![crate::config::AssistantAccountConfig {
name: Some("work".to_owned()),
env: BTreeMap::from([(
"CLAUDE_CONFIG_DIR".to_owned(),
"AION_CLAUDE_WORK_DIR".to_owned(),
)]),
}],
}],
}
.resolved()
}
const CATALOGUE_HARNESS: &str = "claude-code";
#[tokio::test]
async fn an_unparseable_session_id_is_a_400_naming_the_text() -> TestResult {
let state = assistant_state(stock_assistant()).await?;
let response = workflow_router(state.clone())
.oneshot(get_request("/assistant/sessions/not-a-session-id")?)
.await?;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body: Value = read_json(response).await?;
let message = body["message"].as_str().unwrap_or_default().to_owned();
assert!(
message.contains("not-a-session-id"),
"the refusal must name the text that was rejected: {body}"
);
Ok(())
}
#[tokio::test]
async fn a_session_id_that_names_nothing_is_a_404() -> TestResult {
let absent = AssistantSessionId::new(uuid::Uuid::from_u128(0x5e5_5107));
let state = assistant_state(stock_assistant()).await?;
let response = workflow_router(state.clone())
.oneshot(get_request(&format!("/assistant/sessions/{absent}"))?)
.await?;
assert_eq!(response.status(), StatusCode::NOT_FOUND);
Ok(())
}
#[tokio::test]
async fn current_is_a_route_rather_than_a_session_id() -> TestResult {
let state = assistant_state(stock_assistant()).await?;
let response = workflow_router(state.clone())
.oneshot(get_request("/assistant/sessions/current")?)
.await?;
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"a caller with no session has no current one"
);
let body: Value = read_json(response).await?;
let message = body["message"].as_str().unwrap_or_default().to_owned();
assert!(
!message.contains("is not an assistant session id"),
"`current` was parsed as a session id, so the literal route never ran: {body}"
);
assert!(
message.contains("no assistant session"),
"the refusal must say the caller holds no current session: {body}"
);
Ok(())
}
#[tokio::test]
async fn a_stock_server_serves_the_assistant_and_publishes_the_whole_catalogue() -> TestResult {
let state = assistant_state(stock_assistant()).await?;
let described = workflow_router(state.clone())
.oneshot(get_request("/assistant")?)
.await?;
assert_eq!(described.status(), StatusCode::OK);
let body: Value = read_json(described).await?;
assert_eq!(
body["sessions_enabled"],
serde_json::json!(true),
"a server with no [assistant] section must still serve the assistant: {body}"
);
assert!(
body["sessions_disabled_reason"].is_null(),
"`not configured` is not a reason any more; the field is for a fault: {body}"
);
assert!(
body["default_harness"].is_null(),
"a caller who has opened nothing has picked nothing: {body}"
);
let harnesses = body["harnesses"]
.as_array()
.ok_or("the description must carry a `harnesses` array")?;
assert!(
harnesses.len() >= 5,
"the catalogue ships Claude Code, Codex, Gemini CLI, OpenCode and Pi: {body}"
);
for (published, entry) in harnesses.iter().zip(catalogue::CATALOGUE) {
assert_eq!(published["name"], serde_json::json!(entry.id));
assert_eq!(published["kind"], serde_json::json!("acp"));
assert_eq!(
published["launch"],
serde_json::json!(entry.launch()),
"the published line must be the catalogue's own, so what an operator reads is what \
the server runs: {published}"
);
assert_eq!(
published["accounts"],
serde_json::json!([]),
"a stock server declares no account: {published}"
);
let available = published["available"]
.as_bool()
.ok_or("every entry states whether it is available")?;
assert_eq!(
available,
entry.available(),
"`{}` is published as available: {available}, measured: {}",
entry.id,
entry.available()
);
if available {
assert!(
published["install_hint"].is_null(),
"an available harness needs no install hint: {published}"
);
} else {
assert_eq!(
published["install_hint"],
serde_json::json!(entry.install_hint),
"an unavailable harness carries the catalogue's own sentence: {published}"
);
}
}
Ok(())
}
#[tokio::test]
async fn a_declared_account_is_published_on_the_catalogue_harness_it_names() -> TestResult {
let state = assistant_state(assistant_with_an_account()).await?;
let response = workflow_router(state.clone())
.oneshot(get_request("/assistant")?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let body: Value = read_json(response).await?;
let harnesses = body["harnesses"]
.as_array()
.ok_or("the description must carry a `harnesses` array")?;
let declared = harnesses
.iter()
.find(|entry| entry["name"] == serde_json::json!(CATALOGUE_HARNESS))
.ok_or("the catalogue entry the section names must still be published")?;
assert_eq!(
declared["accounts"],
serde_json::json!(["work"]),
"a harness's declared accounts are what a client offers: {body}"
);
let others: Vec<&Value> = harnesses
.iter()
.filter(|entry| entry["name"] != serde_json::json!(CATALOGUE_HARNESS))
.collect();
assert!(
others
.iter()
.all(|entry| entry["accounts"] == serde_json::json!([])),
"declaring an account on one harness must not put it on the others: {body}"
);
Ok(())
}
#[tokio::test]
async fn creating_a_session_on_a_harness_this_machine_cannot_run_is_refused_with_the_hint()
-> TestResult {
for entry in catalogue::CATALOGUE {
let state = assistant_state(stock_assistant()).await?;
let response = workflow_router(state.clone())
.oneshot(json_request(
"/assistant/sessions",
&serde_json::json!({ "harness": entry.id, "account": null, "title": null }),
)?)
.await?;
let status = response.status();
let body: Value = read_json(response).await?;
if entry.available() {
assert_eq!(
status,
StatusCode::CREATED,
"`{}` resolves on this server's PATH, so a session opens on it: {body}",
entry.id
);
assert_eq!(body["harness"], serde_json::json!(entry.id));
} else {
assert_eq!(
status,
StatusCode::SERVICE_UNAVAILABLE,
"`{}` does not resolve on this server's PATH, so the session must be refused \
NOW: {body}",
entry.id
);
let message = body["message"].as_str().unwrap_or_default();
assert!(
message.contains(&entry.launch()),
"the refusal names the line that could not be run: {body}"
);
assert!(
message.contains(entry.install_hint),
"the refusal carries the catalogue's install hint: {body}"
);
}
}
Ok(())
}
#[tokio::test]
async fn the_descriptor_reports_the_harness_this_caller_last_opened() -> TestResult {
let Some(available) = catalogue::CATALOGUE.iter().find(|entry| entry.available()) else {
tracing::info!(
"skipping: no catalogue harness resolves on this venue's PATH, so no session can be \
created to record a pick"
);
return Ok(());
};
let state = assistant_state(stock_assistant()).await?;
let router = workflow_router(state.clone());
let created = router
.clone()
.oneshot(json_request(
"/assistant/sessions",
&serde_json::json!({ "harness": available.id, "account": null, "title": null }),
)?)
.await?;
assert_eq!(created.status(), StatusCode::CREATED);
let described = router.oneshot(get_request("/assistant")?).await?;
assert_eq!(described.status(), StatusCode::OK);
let body: Value = read_json(described).await?;
assert_eq!(
body["default_harness"],
serde_json::json!(available.id),
"the descriptor must report what this caller last opened: {body}"
);
Ok(())
}
#[tokio::test]
async fn the_descriptor_names_the_assistant_tool_and_the_credential_that_reaches_it() -> TestResult
{
let state = assistant_state(assistant_with_an_account()).await?;
let response = workflow_router(state.clone())
.oneshot(get_request("/assistant")?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let body: Value = read_json(response).await?;
let assistant = &body["tools"]["assistant"];
assert_eq!(
assistant["tools"],
serde_json::json!(crate::assistant::mcp::SESSION_TOOL_NAMES),
"the assistant tool server's catalogue must be published by name, every session tool in \
catalogue order: {body}"
);
assert_eq!(
assistant["route"],
serde_json::json!(crate::assistant::mcp::ASSISTANT_MCP_PATH),
"the route the tool is served on must be published: {body}"
);
assert_eq!(assistant["server"], serde_json::json!("assistant"));
let token = &assistant["token"];
assert_eq!(
token["kind"],
serde_json::json!(crate::assistant::mcp::SESSION_TOKEN_KIND)
);
assert_eq!(token["minted_by"], serde_json::json!("server"));
assert_eq!(token["scope"], serde_json::json!("session"));
let described = token["description"]
.as_str()
.ok_or("the credential must carry its description")?;
assert_eq!(
described,
crate::assistant::mcp::SESSION_TOKEN_DESCRIPTION,
"ONE sentence describes this credential; the descriptor must quote it, not restate it"
);
assert!(
described.contains("server-minted") && described.contains("session-scoped"),
"the descriptor's sentence must say server-minted and session-scoped: {described}"
);
Ok(())
}
#[test]
fn the_published_assistant_server_name_is_the_one_a_spawn_hands_over() {
assert_eq!(
crate::assistant::sessions::launch::ASSISTANT_MCP_SERVER_NAME,
"assistant",
"the descriptor publishes this name; a spawn must hand over the same one"
);
}
#[test]
fn every_published_harness_is_one_the_adapter_can_actually_run() {
assert!(
!catalogue::CATALOGUE.is_empty(),
"a build that shipped an empty catalogue would publish an assistant nobody can start"
);
for entry in catalogue::CATALOGUE {
assert!(
catalogue::harness(entry.id).is_some(),
"`{}` is published but does not resolve through the one lookup a create request uses",
entry.id
);
assert!(
!entry.launch().trim().is_empty(),
"`{}` publishes an empty launch line",
entry.id
);
}
}
#[tokio::test]
async fn the_descriptor_lists_every_word_in_the_grant_vocabulary() -> TestResult {
assert!(
!GRANT_WORDS.is_empty(),
"the grant vocabulary is empty, so this pin measures nothing"
);
let state = assistant_state(stock_assistant()).await?;
let response = workflow_router(state.clone())
.oneshot(get_request("/assistant")?)
.await?;
assert_eq!(response.status(), StatusCode::OK);
let body: Value = read_json(response).await?;
let listed = body["grants"]
.as_array()
.ok_or("the description must carry a `grants` array")?;
assert_eq!(
listed.len(),
GRANT_WORDS.len(),
"`grants` lists {} words for a vocabulary of {}: {body}",
listed.len(),
GRANT_WORDS.len()
);
for grant in GRANT_WORDS {
let described = listed
.iter()
.find(|row| row["name"] == serde_json::json!(grant.word()))
.ok_or_else(|| {
format!(
"`{}` is in the grant vocabulary but not in the assistant description",
grant.word()
)
})?;
assert_eq!(
described["description"],
serde_json::json!(grant.description()),
"`{}` is described with words the grammar does not carry",
grant.word()
);
assert!(
!grant.description().trim().is_empty(),
"`{}` reaches the console with nothing to explain it",
grant.word()
);
assert_eq!(
described["held"],
serde_json::json!(true),
"the auth-off operator must hold `{}`",
grant.word()
);
}
Ok(())
}
#[test]
fn every_refusal_variant_carries_a_status() {
let census = every_variant();
let mut seen = Vec::with_capacity(census.len());
for error in &census {
let discriminant = std::mem::discriminant(error);
assert!(
!seen.contains(&discriminant),
"the census lists one variant twice, so it covers fewer than it appears to: {error}"
);
seen.push(discriminant);
let (status, wire) = session_refusal(error);
assert_eq!(
status,
expected_status(error),
"`{error}` is answered with the wrong status"
);
assert!(
!wire.message.trim().is_empty(),
"`{error}` maps to a refusal with no message"
);
assert!(
wire.error_type.is_some(),
"`{error}` maps to a refusal a client cannot branch on"
);
}
}
#[test]
fn another_subjects_session_is_indistinguishable_from_an_absent_one() {
let session_id = AssistantSessionId::new(uuid::Uuid::from_u128(0xf00d));
let (absent_status, absent) = session_refusal(&AssistantSessionError::NotFound { session_id });
let (denied_status, denied) = session_refusal(&AssistantSessionError::NotYours {
session_id,
subject: "someone-else".to_owned(),
});
assert_eq!(absent_status, StatusCode::NOT_FOUND);
assert_eq!(denied_status, StatusCode::NOT_FOUND);
assert_eq!(
absent.message, denied.message,
"the two answers differ, so the id space tells a caller which sessions exist"
);
assert_eq!(absent.error_type, denied.error_type);
assert!(
!denied.message.contains("someone-else"),
"the refusal names the owning subject: {}",
denied.message
);
}
fn every_variant() -> Vec<AssistantSessionError> {
let session_id = AssistantSessionId::new(uuid::Uuid::from_u128(1));
vec![
AssistantSessionError::NotCommissioned {
reason: "the store could not be read".to_owned(),
},
AssistantSessionError::UnknownHarness {
requested: "ghost".to_owned(),
declared: "claude-code, codex".to_owned(),
},
AssistantSessionError::HarnessUnavailable {
harness: "opencode".to_owned(),
launch: "opencode acp".to_owned(),
install_hint: "install OpenCode".to_owned(),
},
AssistantSessionError::AccountEnvironmentAbsent {
harness: "claude-code".to_owned(),
account: "work".to_owned(),
variables: "`AION_CLAUDE_WORK_DIR`".to_owned(),
},
AssistantSessionError::UnknownAccount {
harness: "acme".to_owned(),
requested: "ghost".to_owned(),
declared: "work".to_owned(),
},
AssistantSessionError::NotFound { session_id },
AssistantSessionError::NotYours {
session_id,
subject: "alice".to_owned(),
},
AssistantSessionError::UnknownCommand {
session_id,
requested: "compact".to_owned(),
advertised: "none".to_owned(),
},
AssistantSessionError::UnknownConfigOption {
session_id,
requested: "model".to_owned(),
advertised: "none".to_owned(),
},
AssistantSessionError::Busy { session_id },
AssistantSessionError::Ended {
session_id,
reason: "the agent exited".to_owned(),
},
AssistantSessionError::HarnessFailed {
harness: "acme".to_owned(),
reason: "the binary is not executable".to_owned(),
},
AssistantSessionError::AuthRequired {
harness: "acme".to_owned(),
account: "work".to_owned(),
},
AssistantSessionError::Store(StoreError::Backend("disk is gone".to_owned())),
AssistantSessionError::Internal("a frame is not encodable".to_owned()),
]
}
fn expected_status(error: &AssistantSessionError) -> StatusCode {
match error {
AssistantSessionError::NotFound { .. } | AssistantSessionError::NotYours { .. } => {
StatusCode::NOT_FOUND
}
AssistantSessionError::Busy { .. } | AssistantSessionError::Ended { .. } => {
StatusCode::CONFLICT
}
AssistantSessionError::UnknownHarness { .. }
| AssistantSessionError::UnknownAccount { .. }
| AssistantSessionError::UnknownCommand { .. }
| AssistantSessionError::UnknownConfigOption { .. } => StatusCode::BAD_REQUEST,
AssistantSessionError::NotCommissioned { .. }
| AssistantSessionError::HarnessUnavailable { .. }
| AssistantSessionError::AccountEnvironmentAbsent { .. } => StatusCode::SERVICE_UNAVAILABLE,
AssistantSessionError::AuthRequired { .. }
| AssistantSessionError::HarnessFailed { .. } => StatusCode::BAD_GATEWAY,
AssistantSessionError::Store(_) | AssistantSessionError::Internal(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}