use super::create_test_client;
use super::should_run;
use opencode_rs::types::message::PromptPart;
use opencode_rs::types::message::PromptRequest;
use opencode_rs::types::message::ShellRequest;
use opencode_rs::types::new_message_id;
use opencode_rs::types::permission::PermissionReply;
use opencode_rs::types::permission::PermissionReplyRequest;
use opencode_rs::types::question::QuestionInfo;
use opencode_rs::types::question::QuestionReply;
use opencode_rs::types::session::SessionInitRequest;
use tokio::time::Duration;
fn answer_for_question(question: &QuestionInfo) -> Vec<String> {
let text = question.question.to_ascii_lowercase();
if text.contains("yes") && text.contains("no") {
return vec!["yes".to_string()];
}
if let Some(first_option) = question.options.first() {
return vec![first_option.label.clone()];
}
vec!["yes".to_string()]
}
fn format_error_chain(err: &dyn std::error::Error) -> String {
use std::fmt::Write;
let mut output = format!("{err}");
let mut source = err.source();
while let Some(s) = source {
let _ = write!(output, "\n caused by: {s}");
source = s.source();
}
output
}
async fn drain_questions(
client: &opencode_rs::Client,
session_id: &str,
) -> opencode_rs::error::Result<()> {
let pending = client.question().list().await?;
for request in pending
.into_iter()
.filter(|request| request.session_id == session_id)
{
let answers = request.questions.iter().map(answer_for_question).collect();
client
.question()
.reply(&request.id, &QuestionReply { answers })
.await?;
}
Ok(())
}
async fn drain_permissions(
client: &opencode_rs::Client,
session_id: &str,
) -> opencode_rs::error::Result<()> {
let pending = client.permissions().list().await?;
for request in pending
.into_iter()
.filter(|request| request.session_id == session_id)
{
client
.permissions()
.reply(
&request.id,
&PermissionReplyRequest {
reply: PermissionReply::Always,
message: None,
},
)
.await?;
}
Ok(())
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_crud_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
assert!(!session.id.is_empty(), "Session should have ID");
let fetched = client
.sessions()
.get(&session.id)
.await
.expect("Failed to get session");
assert_eq!(fetched.id, session.id, "Session IDs should match");
match client.sessions().list().await {
Ok(sessions) => {
println!("Listed {} sessions", sessions.len());
if let Some(first) = sessions.first() {
assert!(!first.id.is_empty(), "Session should have ID");
}
}
Err(e) => {
println!("List sessions: {e:?}");
}
}
client
.sessions()
.delete(&session.id)
.await
.expect("Failed to delete session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_prompt_typed_response() {
if !should_run() {
return;
}
let client = create_test_client().await;
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
let response = client
.messages()
.prompt(
&session.id,
&PromptRequest {
parts: vec![PromptPart::Text {
text: "Say hello".to_string(),
synthetic: None,
ignored: None,
metadata: None,
}],
message_id: None,
model: None,
agent: None,
no_reply: Some(true), system: None,
variant: None,
},
)
.await
.expect("Failed to send prompt");
println!("Prompt response status: {:?}", response.status);
println!("Prompt response message_id: {:?}", response.message_id);
let _ = client.sessions().delete(&session.id).await;
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_providers_list_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let response = client
.providers()
.list()
.await
.expect("Failed to list providers");
for provider in &response.all {
assert!(!provider.id.is_empty(), "Provider should have ID");
assert!(!provider.name.is_empty(), "Provider should have name");
println!(
"Provider: {} ({}) - {:?} models",
provider.name,
provider.id,
provider.models.len()
);
}
println!(
"Response: {} providers, {} defaults, {} connected",
response.all.len(),
response.default.len(),
response.connected.len()
);
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_mcp_status_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let status = client
.mcp()
.status()
.await
.expect("Failed to get MCP status");
println!("MCP servers: {:?}", status.servers.len());
for server in &status.servers {
assert!(!server.name.is_empty(), "MCP server should have name");
println!(" Server: {} - {:?}", server.name, server.status);
}
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_lsp_status_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let servers = client.misc().lsp().await.expect("Failed to get LSP status");
println!("LSP servers: {} configured", servers.len());
for server in &servers {
println!(" {} ({}): {:?}", server.name, server.id, server.status);
}
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_formatter_status_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let formatters = client
.misc()
.formatter()
.await
.expect("Failed to get formatter status");
println!("Formatters: {} configured", formatters.len());
for fmt in &formatters {
println!(
" {} - enabled: {}, extensions: {:?}",
fmt.name, fmt.enabled, fmt.extensions
);
}
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_openapi_doc_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let doc = client
.misc()
.doc()
.await
.expect("Failed to get OpenAPI doc");
assert!(doc.spec.is_object(), "Doc should be a JSON object");
assert!(
doc.spec.get("openapi").is_some() || doc.spec.get("swagger").is_some(),
"Should be an OpenAPI/Swagger document"
);
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_find_endpoints_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
match client.find().text("fn").await {
Ok(text_results) => {
println!("Text search: got response");
let _ = text_results.results;
}
Err(e) => {
println!("Text search not available: {e:?}");
}
}
match client.find().files("Cargo").await {
Ok(file_results) => {
println!("File search: got response");
let _ = file_results.results;
}
Err(e) => {
println!("File search not available: {e:?}");
}
}
match client.find().symbols("main").await {
Ok(symbol_results) => {
println!("Symbol search: got response");
let _ = symbol_results.results;
}
Err(e) => {
println!("Symbol search not available: {e:?}");
}
}
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_message_parts_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
let _ = client
.messages()
.prompt(
&session.id,
&PromptRequest {
parts: vec![PromptPart::Text {
text: "Hello".to_string(),
synthetic: None,
ignored: None,
metadata: None,
}],
message_id: None,
model: None,
agent: None,
no_reply: Some(true),
system: None,
variant: None,
},
)
.await;
let messages = client
.messages()
.list(&session.id)
.await
.expect("Failed to list messages");
for message in &messages {
println!("Message {} has {} parts", message.id(), message.parts.len());
for part in &message.parts {
match part {
opencode_rs::types::Part::Text { text, .. } => {
let preview: String = text.chars().take(50).collect();
println!(" Text part: {preview}...");
}
opencode_rs::types::Part::Tool { tool, state, .. } => {
println!(
" Tool part: {} - state: {:?}",
tool,
state.as_ref().map(opencode_rs::types::ToolState::status)
);
}
_ => {
println!(" Other part type");
}
}
}
}
let _ = client.sessions().delete(&session.id).await;
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_init_required_body() {
if !should_run() {
return;
}
let client = create_test_client().await;
let providers = client.providers().list().await.expect("providers list");
let mut connected_ids = providers.connected.clone();
connected_ids.sort();
connected_ids.dedup();
let mut selected = None;
for provider_id in connected_ids {
let Some(provider) = providers
.all
.iter()
.find(|provider| provider.id == provider_id)
else {
continue;
};
let model_id = match providers.default.get(&provider_id) {
Some(default_model_id) if provider.models.contains_key(default_model_id) => {
default_model_id.clone()
}
_ => {
let mut model_ids: Vec<String> = provider.models.keys().cloned().collect();
model_ids.sort();
let Some(first_model_id) = model_ids.into_iter().next() else {
continue;
};
first_model_id
}
};
selected = Some((provider_id, model_id));
break;
}
let Some((provider_id, model_id)) = selected else {
println!(
"Skipping session.init required-body test: no connected providers with usable models (connected={:?})",
providers.connected
);
return;
};
println!("session.init test using provider={provider_id} model={model_id}");
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
let _ = client
.messages()
.prompt(
&session.id,
&PromptRequest {
parts: vec![PromptPart::Text {
text: "Initialize this session".to_string(),
synthetic: None,
ignored: None,
metadata: None,
}],
message_id: None,
model: None,
agent: None,
no_reply: Some(true),
system: None,
variant: None,
},
)
.await
.expect("Failed to create a message for session.init preconditions");
let session_id = session.id.clone();
let init_message_id = new_message_id();
let init_task = tokio::spawn({
let client = client.clone();
let session_id = session_id.clone();
let provider_id = provider_id.clone();
let model_id = model_id.clone();
async move {
client
.sessions()
.init(
&session_id,
&SessionInitRequest {
model_id,
provider_id,
message_id: init_message_id,
},
)
.await
}
});
let mut init_task = std::pin::pin!(init_task);
let mut last_step = String::from("spawned session.init task");
let driver = async {
last_step = "initial drain of questions".to_string();
let _ = drain_questions(&client, &session_id).await;
last_step = "initial drain of permissions".to_string();
let _ = drain_permissions(&client, &session_id).await;
loop {
tokio::select! {
join_result = &mut init_task => {
last_step = "session.init task completed".to_string();
let ok = match join_result {
Ok(result) => result?,
Err(error) => {
return Err(opencode_rs::error::OpencodeError::State(format!(
"session.init task join error: {error}"
)));
}
};
return Ok::<bool, opencode_rs::error::OpencodeError>(ok);
}
() = tokio::time::sleep(Duration::from_millis(50)) => {
last_step = "polling questions".to_string();
drain_questions(&client, &session_id).await?;
last_step = "polling permissions".to_string();
drain_permissions(&client, &session_id).await?;
}
}
}
};
let ok = match tokio::time::timeout(Duration::from_secs(300), driver).await {
Ok(Ok(ok)) => ok,
Ok(Err(error)) => {
init_task.as_mut().abort();
let _ = client.sessions().delete(&session_id).await;
panic!(
"session.init driver should succeed: {}",
format_error_chain(&error)
);
}
Err(_) => {
init_task.as_mut().abort();
let _ = client.sessions().delete(&session_id).await;
panic!("session.init timed out after 5 minutes; last observed step: {last_step}");
}
};
assert!(ok, "session.init should return true");
let deleted = client
.sessions()
.delete(&session_id)
.await
.expect("session.init test cleanup should delete the session");
assert!(deleted, "session.init cleanup should delete the session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_diff_patch_typed() {
if !should_run() {
return;
}
let client = create_test_client().await;
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
let diff = client.sessions().diff(&session.id).await;
println!("session.diff result: {diff:?}");
let _ = client.sessions().delete(&session.id).await;
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_shell_returns_info_and_parts() {
if !should_run() {
return;
}
let client = create_test_client().await;
let mut agents = client
.tools()
.agents()
.await
.expect("Failed to list agents");
agents.sort_by(|a, b| a.name.cmp(&b.name));
let Some(agent) = agents.into_iter().next() else {
println!("Skipping shell test: no agents configured on the live server");
return;
};
let agent_name = agent.name;
println!("shell test using agent={agent_name}");
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
let shell = client
.messages()
.shell(
&session.id,
&ShellRequest {
agent: agent_name,
command: "echo hello".to_string(),
message_id: None,
model: None,
},
)
.await;
let _ = client.sessions().delete(&session.id).await;
let shell = shell.expect("Shell call should succeed with upstream-compatible request shape");
assert!(
!shell.info.id.is_empty(),
"shell response should include info.id"
);
assert!(
!shell.parts.is_empty(),
"shell response should include at least one part"
);
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_permission_ruleset() {
if !should_run() {
return;
}
let client = create_test_client().await;
let session = client
.sessions()
.create(&Default::default())
.await
.expect("Failed to create session");
let fetched = client
.sessions()
.get(&session.id)
.await
.expect("Failed to get session");
if let Some(permission) = &fetched.permission {
println!("Session has {} permission rules", permission.len());
for rule in permission {
println!(
" Rule: {} {} {:?}",
rule.permission, rule.pattern, rule.action
);
}
}
let _ = client.sessions().delete(&session.id).await;
}