use crate::backend::{TtyBackend, TtyError, TtyHandle, TtyParams};
use async_trait::async_trait;
pub struct LocalTtyBackend;
impl LocalTtyBackend {
pub fn new() -> Self {
Self
}
}
impl Default for LocalTtyBackend {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl TtyBackend for LocalTtyBackend {
async fn allocate(&self, params: &TtyParams) -> Result<TtyHandle, TtyError> {
if params.cmd.is_empty() {
return Err(TtyError::AllocFailed {
message: "cmd must be non-empty".to_string(),
});
}
match ¶ms.terminal {
Some(terminal) => crate::local::pty::allocate_pty(
terminal.clone(),
params.cmd.clone(),
params.cwd.clone(),
params.env.clone(),
),
None => crate::local::pipe::allocate_pipe(
params.cmd.clone(),
params.cwd.clone(),
params.env.clone(),
),
}
}
fn resource_id(&self, _params: &TtyParams) -> Option<(&'static str, String)> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn term() -> crate::backend::TerminalParams {
crate::backend::TerminalParams {
term: None,
cols: 80,
rows: 24,
pixel_width: 0,
pixel_height: 0,
modes: serde_json::Value::Null,
}
}
fn params(terminal: Option<crate::backend::TerminalParams>, cmd: Vec<String>) -> TtyParams {
TtyParams {
terminal,
cmd,
cwd: None,
env: HashMap::new(),
backend_params: serde_json::Map::new(),
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pty_dispatch_yields_no_stderr() {
let backend = LocalTtyBackend::new();
let handle = backend
.allocate(¶ms(
Some(term()),
vec!["echo".to_string(), "hi".to_string()],
))
.await
.expect("allocate");
assert!(handle.stderr.is_none(), "PTY backends merge stdout/stderr");
assert!(handle.control.is_some(), "PTY backends provide control");
let _ = handle.exit_code.await;
}
#[tokio::test]
async fn pipe_dispatch_yields_some_stderr() {
let backend = LocalTtyBackend::new();
let handle = backend
.allocate(¶ms(None, vec!["echo".to_string(), "hi".to_string()]))
.await
.expect("allocate");
assert!(
handle.stderr.is_some(),
"pipe backends have separate stderr"
);
assert!(handle.control.is_some(), "pipe backends provide control");
let _ = handle.exit_code.await;
}
#[tokio::test]
async fn empty_cmd_returns_alloc_failed() {
let backend = LocalTtyBackend::new();
let result = backend.allocate(¶ms(None, vec![])).await;
assert!(
matches!(result, Err(TtyError::AllocFailed { .. })),
"expected AllocFailed for empty cmd"
);
}
#[tokio::test]
async fn empty_cmd_with_terminal_returns_alloc_failed() {
let backend = LocalTtyBackend::new();
let result = backend.allocate(¶ms(Some(term()), vec![])).await;
assert!(
matches!(result, Err(TtyError::AllocFailed { .. })),
"expected AllocFailed for empty cmd even with terminal set"
);
}
#[test]
fn resource_id_returns_none() {
let backend = LocalTtyBackend::new();
let params = params(None, vec!["true".to_string()]);
assert!(backend.resource_id(¶ms).is_none());
}
#[test]
fn resource_id_returns_none_with_terminal() {
let backend = LocalTtyBackend::new();
let params = params(Some(term()), vec!["true".to_string()]);
assert!(backend.resource_id(¶ms).is_none());
}
#[test]
fn new_constructs() {
let backend = LocalTtyBackend::new();
let params = params(None, vec!["true".to_string()]);
assert!(backend.resource_id(¶ms).is_none());
}
}