use core::net::IpAddr;
use core::result::Result as CoreResult;
use embedded_io_async::{ErrorType, Read};
use embedded_nal_async::{AddrType, Dns, TcpConnect};
use ocpncord_backend::*;
use ocpncord_backend_opencode::OpenCodeBackend;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
struct StdTcp;
impl TcpConnect for StdTcp {
type Error = std::io::Error;
type Connection<'a> = StdTcpStream;
async fn connect<'a>(
&'a self,
remote: core::net::SocketAddr,
) -> CoreResult<Self::Connection<'a>, Self::Error> {
let stream = tokio::net::TcpStream::connect(remote).await?;
Ok(StdTcpStream(stream))
}
}
struct StdDns;
impl Dns for StdDns {
type Error = std::io::Error;
async fn get_host_by_name(
&self,
host: &str,
addr_type: AddrType,
) -> CoreResult<IpAddr, Self::Error> {
let addrs = tokio::net::lookup_host((host, 0)).await?;
let addrs: Vec<std::net::SocketAddr> = addrs.collect();
let addr = match addr_type {
AddrType::IPv4 => addrs.iter().find(|a| a.is_ipv4()),
AddrType::IPv6 => addrs.iter().find(|a| a.is_ipv6()),
AddrType::Either => addrs
.iter()
.find(|a| a.is_ipv4())
.or_else(|| addrs.iter().find(|a| a.is_ipv6())),
};
match addr {
Some(a) => Ok(a.ip()),
None => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no address found for host",
)),
}
}
async fn get_host_by_address(
&self,
_addr: IpAddr,
_result: &mut [u8],
) -> CoreResult<usize, Self::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"reverse DNS not supported",
))
}
}
struct StdTcpStream(tokio::net::TcpStream);
impl ErrorType for StdTcpStream {
type Error = std::io::Error;
}
impl Read for StdTcpStream {
async fn read(&mut self, buf: &mut [u8]) -> CoreResult<usize, Self::Error> {
self.0.read(buf).await
}
}
impl embedded_io_async::Write for StdTcpStream {
async fn write(&mut self, buf: &[u8]) -> CoreResult<usize, Self::Error> {
self.0.write(buf).await
}
async fn flush(&mut self) -> CoreResult<(), Self::Error> {
self.0.flush().await
}
}
fn backend() -> OpenCodeBackend<StdTcp, StdDns> {
static TCP: StdTcp = StdTcp;
static DNS: StdDns = StdDns;
OpenCodeBackend::new("http://localhost:4096", &TCP, &DNS)
}
#[tokio::test]
async fn health() {
let mut b = backend();
let h = b.health().await.expect("health should succeed");
assert!(h.healthy);
}
#[tokio::test]
async fn create_and_get_session() {
let mut b = backend();
let s = b
.create_session("integration-test", "/tmp")
.await
.expect("create session should succeed");
assert!(!s.id.is_empty());
assert_eq!(s.title, "integration-test");
assert!(!s.slug.is_empty());
assert!(!s.version.is_empty());
assert!(s.time.created > 0);
let fetched = b
.get_session(&s.id)
.await
.expect("get session should succeed");
assert_eq!(fetched.id, s.id);
assert_eq!(fetched.title, s.title);
}
#[tokio::test]
async fn list_sessions() {
let mut b = backend();
let _ = b.create_session("list-test", "/tmp").await;
let sessions = b
.list_sessions()
.await
.expect("list sessions should succeed");
assert!(!sessions.is_empty());
assert!(sessions.iter().any(|s| s.title == "list-test"));
}
#[tokio::test]
async fn update_session() {
let mut b = backend();
let s = b.create_session("update-test", "/tmp").await.unwrap();
let updated = b
.update_session(&s.id, "updated-title")
.await
.expect("update should succeed");
assert_eq!(updated.title, "updated-title");
assert_eq!(updated.id, s.id);
}
#[tokio::test]
async fn children_sessions() {
let mut b = backend();
let parent = b.create_session("parent-test", "/tmp").await.unwrap();
let children = b
.children_sessions(&parent.id)
.await
.expect("children should succeed");
assert!(children.is_empty());
}
#[tokio::test]
async fn abort_session() {
let mut b = backend();
let s = b.create_session("abort-test", "/tmp").await.unwrap();
b.abort_session(&s.id).await.expect("abort should succeed");
}
#[tokio::test]
async fn find_text() {
let mut b = backend();
let matches = b
.find_text("integration")
.await
.expect("find should succeed");
assert!(!matches.is_empty());
assert!(!matches[0].path.text.is_empty());
assert!(matches[0].line_number > 0);
assert!(!matches[0].lines.text.is_empty());
}
#[tokio::test]
async fn get_config() {
let mut b = backend();
let config = b.get_config().await.expect("get config should succeed");
assert!(config.username.is_some());
}
#[tokio::test]
async fn set_auth() {
let mut b = backend();
b.set_auth("integration-test-provider", "sk-test-key")
.await
.expect("set auth should succeed");
}
#[tokio::test]
async fn prompt_and_messages() {
use futures::StreamExt;
let mut b = backend();
let s = b.create_session("prompt-test", "/tmp").await.unwrap();
let mut stream = b
.prompt(&s.id, "say hello in one word", None)
.await
.expect("prompt should succeed");
while let Some(event) = stream.next().await {
match event {
Ok(ocpncord_backend::BackendEvent::Done) => break,
Ok(_) => {}
Err(e) => panic!("unexpected error in prompt stream: {e}"),
}
}
let messages = b
.list_messages(&s.id)
.await
.expect("list messages should succeed");
assert!(
!messages.is_empty(),
"prompt should create at least one message"
);
let msg = &messages[0];
assert!(!msg.id.is_empty());
assert!(!msg.session_id.is_empty());
assert!(msg.time.created > 0);
}
#[tokio::test]
async fn send_command() {
use futures::StreamExt;
let mut b = backend();
let s = b.create_session("command-test", "/tmp").await.unwrap();
let mut stream = b
.command(&s.id, "init", None)
.await
.expect("command should succeed");
while let Some(event) = stream.next().await {
match event {
Ok(ocpncord_backend::BackendEvent::Done) => break,
Ok(_) => {}
Err(e) => panic!("unexpected error in command stream: {e}"),
}
}
}
#[tokio::test]
async fn delete_session() {
let mut b = backend();
let s = b.create_session("delete-test", "/tmp").await.unwrap();
b.delete_session(&s.id)
.await
.expect("delete should succeed");
let sessions = b.list_sessions().await.unwrap();
assert!(!sessions.iter().any(|sess| sess.id == s.id));
}
#[tokio::test]
async fn subscribe_connects() {
let mut b = backend();
let stream = b.subscribe().await.expect("subscribe should succeed");
drop(stream);
}
#[tokio::test]
async fn full_roundtrip() {
let mut b = backend();
let h = b.health().await.unwrap();
assert!(h.healthy);
let s = b.create_session("roundtrip", "/tmp").await.unwrap();
assert!(!s.id.is_empty());
let fetched = b.get_session(&s.id).await.unwrap();
assert_eq!(fetched.id, s.id);
let updated = b.update_session(&s.id, "roundtrip-updated").await.unwrap();
assert_eq!(updated.title, "roundtrip-updated");
let matches = b.find_text("roundtrip").await.unwrap();
assert!(matches.len() >= 1);
let config = b.get_config().await.unwrap();
assert!(config.username.is_some());
b.delete_session(&s.id).await.unwrap();
let sessions = b.list_sessions().await.unwrap();
assert!(!sessions.iter().any(|sess| sess.id == s.id));
}