use std::{
path::{Path, PathBuf},
sync::Arc,
};
use agent_client_protocol::schema::{
ProtocolVersion,
v1::{
ContentBlock, ErrorCode, InitializeRequest, ListSessionsRequest, ResourceLink,
SessionCapabilities, TextContent,
},
};
use super::config::{ServeConfig, SessionSource};
use super::initialize;
use super::lifecycle::{list_sessions, session_info, setup_failed};
use super::turn::prompt_text;
use super::workspaces::{ConfiguredSource, WorkspaceKey};
use crate::mode::ApprovalMode;
use basis::{
ContextConfig, McpServer, PersistedSession, PreparedRun, RunConfig, RunError, Runtime,
ToolsConfig, hooks::HooksConfig, mcp::McpConfig, provider::ProviderError, skills::SkillsConfig,
templates::TemplatesConfig,
};
use mentra::ModelSelector;
struct Ephemeral;
#[async_trait::async_trait]
impl SessionSource for Ephemeral {
async fn create(&self, _cwd: PathBuf, _mcp: Vec<McpServer>) -> Result<PreparedRun, RunError> {
Err(RunError::NoSuchSession)
}
}
fn capabilities(config: &ServeConfig) -> SessionCapabilities {
initialize(&InitializeRequest::new(ProtocolVersion::V1), config)
.agent_capabilities
.session_capabilities
}
#[test]
fn initialize_advertises_resumable_sessions() {
let response = initialize(
&InitializeRequest::new(ProtocolVersion::V1),
&ServeConfig::default(),
);
assert!(
response.agent_capabilities.load_session,
"sessions are persisted mentra agents, so a client can reconnect"
);
assert_eq!(
response.protocol_version,
ProtocolVersion::V1,
"the client's version is echoed, not overridden"
);
}
#[test]
fn initialize_advertises_only_the_session_methods_lan_answers() {
let capabilities = capabilities(&ServeConfig::default());
assert!(capabilities.resume.is_some());
assert!(capabilities.close.is_some());
assert!(
capabilities.list.is_some(),
"the default source reads mentra's store, so it can enumerate"
);
assert!(
capabilities.delete.is_none(),
"mentra's store has no delete; claiming one would promise a deletion that undoes itself"
);
}
#[test]
fn a_source_that_cannot_enumerate_does_not_claim_a_list() {
assert!(
capabilities(&ServeConfig::with_source(Ephemeral))
.list
.is_none()
);
}
#[tokio::test]
async fn a_source_that_cannot_enumerate_refuses_the_call_it_never_claimed() {
let error = list_sessions(
&ServeConfig::with_source(Ephemeral),
ListSessionsRequest::new().cwd(PathBuf::from("/repo")),
)
.await
.expect_err("a source with no registry must not answer for one");
assert_eq!(error.code, ErrorCode::MethodNotFound);
}
#[test]
fn no_authentication_method_is_offered() {
assert!(
initialize(
&InitializeRequest::new(ProtocolVersion::V1),
&ServeConfig::default()
)
.auth_methods
.is_empty()
);
}
#[test]
fn a_listed_session_is_reported_in_the_workspace_it_was_listed_for() {
let info = session_info(
PersistedSession {
agent_id: "agent-1".to_string(),
name: "basis acp".to_string(),
messages: 4,
},
Path::new("/repo"),
);
assert_eq!(
&*info.session_id.0, "agent-1",
"the agent id is the session id"
);
assert_eq!(
info.cwd,
PathBuf::from("/repo"),
"a conversation is in this list because it belongs to this workspace"
);
assert_eq!(info.title.as_deref(), Some("basis acp"));
assert_eq!(
info.updated_at, None,
"mentra exposes no timestamp, and a made-up one would sort a picker by nothing"
);
}
#[test]
fn prompt_text_joins_text_blocks() {
let text = prompt_text(&[
ContentBlock::Text(TextContent::new("first".to_string())),
ContentBlock::Text(TextContent::new("second".to_string())),
]);
assert_eq!(text, "first\nsecond");
}
#[test]
fn a_resource_link_is_named_rather_than_dropped() {
let text = prompt_text(&[ContentBlock::ResourceLink(ResourceLink::new(
"notes.md".to_string(),
"file:///repo/notes.md".to_string(),
))]);
assert!(
text.contains("notes.md") && text.contains("file:///repo/notes.md"),
"what the user attached must survive into the prompt: {text}"
);
}
#[test]
fn an_empty_prompt_produces_no_text() {
assert!(prompt_text(&[]).is_empty());
}
#[test]
fn the_config_template_takes_the_clients_working_directory() {
let source = ConfiguredSource::new(Some(
RunConfig::new("/placeholder", "").with_shell(basis::ShellAccess::Denied),
));
let built = source.config_for(PathBuf::from("/repo"), Vec::new());
assert_eq!(built.workspace, PathBuf::from("/repo"));
assert_eq!(
built.shell,
basis::ShellAccess::Denied,
"everything the client cannot say must carry through"
);
}
#[test]
fn the_clients_mcp_servers_reach_the_config() {
let source = ConfiguredSource::new(None);
let built = source.config_for(
PathBuf::from("/repo"),
vec![McpServer::Stdio(mentra::McpServerConfig {
name: "fs".to_string(),
command: "/bin/mcp-fs".to_string(),
args: Vec::new(),
env: Default::default(),
cwd: None,
})],
);
let supplied: Vec<&str> = built.mcp.supplied.iter().map(McpServer::name).collect();
assert_eq!(
supplied,
vec!["fs"],
"session/new is where a client says which servers it wants"
);
}
#[test]
fn a_session_opens_asking_unless_the_operator_says_otherwise() {
assert_eq!(ServeConfig::default().initial_mode, ApprovalMode::Prompt);
assert_eq!(
ServeConfig::new(RunConfig::new("/repo", "")).initial_mode,
ApprovalMode::Prompt,
"a template says what a run is, not how much it may do without asking"
);
assert_eq!(
ServeConfig::default()
.with_initial_mode(ApprovalMode::Never)
.initial_mode,
ApprovalMode::Never,
"`basis acp --approve never` opens every session read-only"
);
}
#[test]
fn a_missing_credential_is_an_authentication_failure_not_an_internal_one() {
let error = setup_failed(RunError::Provider(ProviderError::NoCredential));
assert_eq!(error.code, ErrorCode::AuthRequired);
assert!(
error
.data
.as_ref()
.and_then(serde_json::Value::as_str)
.is_some_and(|data| data.contains("ANTHROPIC_API_KEY")),
"the actionable part is which variable to set: {:?}",
error.data
);
}
#[test]
fn other_setup_failures_stay_internal_errors() {
let error = setup_failed(RunError::NoSuchSession);
assert_eq!(error.code, ErrorCode::InternalError);
}
fn offline_runtime() -> Arc<Runtime> {
Arc::new(
Runtime::builder()
.with_base_url("http://127.0.0.1:1/v1")
.with_api_key("test-key")
.with_ephemeral_history()
.build()
.expect("a runtime builds without touching the network"),
)
}
fn offline_template() -> RunConfig {
RunConfig::new("/placeholder", "")
.with_model(ModelSelector::Id("test-model".to_string()))
.with_context(ContextConfig {
file_name: "AGENTS.md".to_string(),
global_dir: None,
walk_parents: false,
})
.with_skills(SkillsConfig {
workspace_subdir: PathBuf::from(".basis/skills"),
global_dir: None,
})
.with_templates(TemplatesConfig {
workspace_subdir: PathBuf::from(".basis/templates"),
global_dir: None,
})
.with_hooks(HooksConfig {
workspace_file: PathBuf::from(".basis/hooks.json"),
global_dir: None,
})
.with_tools(ToolsConfig {
workspace_file: PathBuf::from(".basis/tools.json"),
global_dir: None,
})
.with_mcp(McpConfig {
workspace_file: PathBuf::from(".mcp.json"),
global_dir: None,
supplied: Vec::new(),
})
}
#[tokio::test]
async fn two_sessions_on_one_workspace_share_one_runtime() {
let repository = tempfile::tempdir().expect("tempdir");
let runtime = offline_runtime();
let source = ConfiguredSource::on_runtime(Arc::clone(&runtime), Some(offline_template()));
let first = source
.create(repository.path().to_path_buf(), Vec::new())
.await
.expect("the first session opens");
let second = source
.create(repository.path().to_path_buf(), Vec::new())
.await
.expect("the second session opens");
assert_ne!(
first.agent_id(),
second.agent_id(),
"two sessions, not one conversation handed out twice"
);
let opened = source.opened();
assert_eq!(
opened.len(),
1,
"one directory is one workspace, however many sessions were minted on it"
);
assert!(
std::ptr::eq(opened[0].mentra_runtime(), runtime.mentra_runtime()),
"and both were minted on the one runtime this process built"
);
}
#[test]
fn a_session_that_asked_for_different_servers_is_a_different_workspace() {
let source = ConfiguredSource::new(Some(offline_template()));
let server = |command: &str| {
vec![McpServer::Stdio(mentra::McpServerConfig {
name: "fs".to_string(),
command: command.to_string(),
args: Vec::new(),
env: Default::default(),
cwd: None,
})]
};
let key = |mcp| WorkspaceKey::of(&source.config_for(PathBuf::from("/repo"), mcp));
assert_eq!(
key(Vec::new()),
key(Vec::new()),
"the same directory asked about the same way is one workspace"
);
assert_ne!(
key(Vec::new()),
key(server("/bin/mcp-fs")),
"a supplied server is not none"
);
assert_ne!(
key(server("/bin/mcp-fs")),
key(server("/bin/other-fs")),
"one name, two commands: a client that named a different program must get it"
);
assert_ne!(
key(Vec::new()),
WorkspaceKey::of(&source.config_for(PathBuf::from("/other-repo"), Vec::new())),
"and a directory is a key"
);
}