#![allow(clippy::unwrap_used, clippy::expect_used)]
#[path = "integration/mod.rs"]
mod typed_tests;
#[cfg(feature = "server")]
use opencode_rs::server::ManagedServer;
#[cfg(feature = "server")]
use opencode_rs::server::ServerOptions;
#[cfg(feature = "server")]
use opencode_rs::version::OPENCODE_BINARY_ARGS_ENV;
#[cfg(feature = "server")]
use opencode_rs::version::OPENCODE_BINARY_ENV;
#[cfg(feature = "server")]
use std::sync::Arc;
#[cfg(feature = "server")]
use tokio::sync::OnceCell;
use opencode_rs::Client;
use opencode_rs::ClientBuilder;
use opencode_rs::types::event::Event;
use opencode_rs::types::message::PromptPart;
use opencode_rs::types::message::PromptRequest;
use opencode_rs::types::session::CreateSessionRequest;
use std::time::Duration;
fn should_run() -> bool {
std::env::var("OPENCODE_INTEGRATION").is_ok()
}
fn directory() -> String {
std::env::var("OPENCODE_DIRECTORY").unwrap_or_else(|_| {
std::env::current_dir()
.map_or_else(|_| "/tmp".to_string(), |p| p.to_string_lossy().to_string())
})
}
#[cfg(feature = "server")]
static SERVER: OnceCell<Arc<ManagedServer>> = OnceCell::const_new();
#[cfg(feature = "server")]
fn resolve_launcher_config() -> (String, Vec<String>) {
let binary = std::env::var(OPENCODE_BINARY_ENV).unwrap_or_else(|_| "opencode".to_string());
let args = std::env::var(OPENCODE_BINARY_ARGS_ENV)
.map(|s| s.split_whitespace().map(String::from).collect())
.unwrap_or_default();
(binary, args)
}
#[cfg(feature = "server")]
async fn start_server() -> Arc<ManagedServer> {
Arc::clone(
SERVER
.get_or_init(|| async {
let (binary, launcher_args) = resolve_launcher_config();
let dir = directory();
eprintln!(
"[integration] Starting managed server: binary={binary}, args={launcher_args:?}, dir={dir}"
);
let opts = ServerOptions::default()
.binary(&binary)
.launcher_args(launcher_args)
.directory(&dir)
.startup_timeout_ms(30_000);
let server = ManagedServer::start(opts)
.await
.expect("Failed to start managed opencode server");
let url = server.url().to_string();
eprintln!("[integration] Server started at {url}");
unsafe { std::env::set_var("OPENCODE_BASE_URL", &url) };
Arc::new(server)
})
.await,
)
}
#[cfg(feature = "server")]
async fn build_client() -> Client {
let server = start_server().await;
ClientBuilder::new()
.base_url(server.url().as_str())
.directory(directory())
.timeout_secs(300)
.build()
.unwrap()
}
#[cfg(not(feature = "server"))]
fn base_url() -> String {
std::env::var("OPENCODE_BASE_URL").unwrap_or_else(|_| "http://127.0.0.1:4096".to_string())
}
#[cfg(not(feature = "server"))]
#[expect(
clippy::unused_async,
reason = "async for API compatibility with server-enabled version"
)]
async fn build_client() -> Client {
ClientBuilder::new()
.base_url(base_url())
.directory(directory())
.timeout_secs(300)
.build()
.unwrap()
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_server_health() {
if !should_run() {
return;
}
let client = build_client().await;
let health = client.misc().health().await.expect("Failed to get health");
assert!(health.healthy, "Server should be healthy");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_lifecycle() {
if !should_run() {
return;
}
let client = build_client().await;
let session = client
.sessions()
.create(&CreateSessionRequest {
title: Some("Integration Test Session".into()),
..Default::default()
})
.await
.expect("Failed to create session");
assert!(!session.id.is_empty());
let fetched = client
.sessions()
.get(&session.id)
.await
.expect("Failed to get session");
assert_eq!(fetched.id, session.id);
match client.sessions().list().await {
Ok(sessions) => {
println!("Found {} sessions", sessions.len());
}
Err(e) => {
println!("List sessions failed: {e:?}");
}
}
client
.sessions()
.delete(&session.id)
.await
.expect("Failed to delete session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_prompt_and_stream() {
if !should_run() {
return;
}
let client = build_client().await;
let session = client
.sessions()
.create(&CreateSessionRequest::default())
.await
.expect("Failed to create session");
let mut subscription = client
.subscribe_session(&session.id)
.expect("Failed to subscribe");
let prompt_result = client
.messages()
.prompt(
&session.id,
&PromptRequest {
parts: vec![PromptPart::Text {
text: "Say 'hello' and nothing else.".into(),
synthetic: None,
ignored: None,
metadata: None,
}],
message_id: None,
model: None,
agent: None,
no_reply: None,
system: None,
variant: None,
},
)
.await;
if prompt_result.is_err() {
println!("Prompt failed (no provider?): {:?}", prompt_result.err());
subscription.close();
let _ = client.sessions().delete(&session.id).await;
return;
}
let timeout = Duration::from_secs(30);
let start = std::time::Instant::now();
let mut got_any_event = false;
while start.elapsed() < timeout {
tokio::select! {
event = subscription.recv() => {
match event {
Some(Event::SessionIdle { .. } | Event::SessionError { .. }) => {
got_any_event = true;
break;
}
Some(_) => {
got_any_event = true;
}
None => break,
}
}
() = tokio::time::sleep(Duration::from_millis(100)) => {}
}
}
println!(
"Streaming test: got_any_event={}, elapsed={:?}",
got_any_event,
start.elapsed()
);
subscription.close();
client
.sessions()
.delete(&session.id)
.await
.expect("Failed to delete session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_abort() {
if !should_run() {
return;
}
let client = build_client().await;
let session = client
.sessions()
.create(&CreateSessionRequest::default())
.await
.expect("Failed to create session");
let result = client.sessions().abort(&session.id).await;
if let Err(e) = &result {
assert!(!e.is_not_found(), "Session should exist");
}
client
.sessions()
.delete(&session.id)
.await
.expect("Failed to delete session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_permissions_list() {
if !should_run() {
return;
}
let client = build_client().await;
let permissions = client
.permissions()
.list()
.await
.expect("Failed to list permissions");
let _ = permissions.len();
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_files_list() {
if !should_run() {
return;
}
let client = build_client().await;
match client.files().list().await {
Ok(files) => {
let _ = files.len();
}
Err(e) => {
println!("Files list not available: {e:?}");
}
}
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_files_status() {
if !should_run() {
return;
}
let client = build_client().await;
let status = client
.files()
.status()
.await
.expect("Failed to get file status");
let _ = status.len();
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_project_list() {
if !should_run() {
return;
}
let client = build_client().await;
let projects = client
.project()
.list()
.await
.expect("Failed to list projects");
assert!(!projects.is_empty(), "Should have at least one project");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_project_current() {
if !should_run() {
return;
}
let client = build_client().await;
let project = client
.project()
.current()
.await
.expect("Failed to get current project");
assert!(!project.id.is_empty(), "Project should have an ID");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_providers_list() {
if !should_run() {
return;
}
let client = build_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 an ID");
assert!(!provider.name.is_empty(), "Provider should have a name");
}
println!(
"Found {} providers, {} defaults, {} connected",
response.all.len(),
response.default.len(),
response.connected.len()
);
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_mcp_status() {
if !should_run() {
return;
}
let client = build_client().await;
let status = client
.mcp()
.status()
.await
.expect("Failed to get MCP status");
let _ = status.servers.len();
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_config_get() {
if !should_run() {
return;
}
let client = build_client().await;
let config = client.config().get().await.expect("Failed to get config");
assert!(config.extra.is_object() || config.extra.is_null());
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_agents_list() {
if !should_run() {
return;
}
let client = build_client().await;
let agents = client
.tools()
.agents()
.await
.expect("Failed to list agents");
assert!(!agents.is_empty(), "Should have at least one agent");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_commands_list() {
if !should_run() {
return;
}
let client = build_client().await;
let commands = client
.tools()
.commands()
.await
.expect("Failed to list commands");
let _ = commands.len();
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_vcs_info() {
if !should_run() {
return;
}
let client = build_client().await;
let vcs = client.misc().vcs().await.expect("Failed to get VCS info");
println!("VCS type: {:?}", vcs.r#type);
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_path_info() {
if !should_run() {
return;
}
let client = build_client().await;
let path = client.misc().path().await.expect("Failed to get path info");
assert!(!path.directory.is_empty(), "Directory should not be empty");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_openapi_doc() {
if !should_run() {
return;
}
let client = build_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_session_fork() {
if !should_run() {
return;
}
let client = build_client().await;
let parent = client
.sessions()
.create(&CreateSessionRequest {
title: Some("Parent Session".into()),
..Default::default()
})
.await
.expect("Failed to create parent session");
let forked = client
.sessions()
.fork(&parent.id)
.await
.expect("Failed to fork session");
assert_ne!(forked.id, parent.id, "Forked session should have new ID");
client
.sessions()
.delete(&forked.id)
.await
.expect("Failed to delete forked session");
client
.sessions()
.delete(&parent.id)
.await
.expect("Failed to delete parent session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_children() {
if !should_run() {
return;
}
let client = build_client().await;
let parent = client
.sessions()
.create(&CreateSessionRequest {
title: Some("Parent Session".into()),
..Default::default()
})
.await
.expect("Failed to create parent session");
let child = client
.sessions()
.fork(&parent.id)
.await
.expect("Failed to fork session");
let children = client
.sessions()
.children(&parent.id)
.await
.expect("Failed to get children");
let _ = children.len();
client
.sessions()
.delete(&child.id)
.await
.expect("Failed to delete child session");
client
.sessions()
.delete(&parent.id)
.await
.expect("Failed to delete parent session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_diff() {
if !should_run() {
return;
}
let client = build_client().await;
let session = client
.sessions()
.create(&CreateSessionRequest::default())
.await
.expect("Failed to create session");
let diff = client
.sessions()
.diff(&session.id)
.await
.expect("Failed to get session diff");
println!("Session diff has {} file entries", diff.len());
client
.sessions()
.delete(&session.id)
.await
.expect("Failed to delete session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_todos() {
if !should_run() {
return;
}
let client = build_client().await;
let session = client
.sessions()
.create(&CreateSessionRequest::default())
.await
.expect("Failed to create session");
let todos = client
.sessions()
.todo(&session.id)
.await
.expect("Failed to get todos");
let _ = todos.len();
client
.sessions()
.delete(&session.id)
.await
.expect("Failed to delete session");
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_global_events() {
if !should_run() {
return;
}
let client = build_client().await;
let mut subscription = client
.subscribe_global()
.expect("Failed to subscribe to global events");
let timeout = Duration::from_secs(5);
let start = std::time::Instant::now();
let mut got_event = false;
while start.elapsed() < timeout {
tokio::select! {
event = subscription.recv() => {
match event {
Some(_) => {
got_event = true;
break;
}
None => break,
}
}
() = tokio::time::sleep(Duration::from_millis(100)) => {}
}
}
subscription.close();
if got_event {
println!("Received global event");
} else {
println!("No global events received within timeout (this is OK)");
}
}
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_health_returns_pinned_version() {
if !should_run() {
return;
}
let client = build_client().await;
let health = client.misc().health().await.expect("health check failed");
opencode_rs::version::validate_exact_version(health.version.as_deref())
.expect("server version must match SDK pinned version");
}