mod common;
use std::path::PathBuf;
use agentos_client::{AgentOs, AgentOsConfig, OpenShellOptions, PackageRef, StdinInput};
fn coreutils_package_path() -> Option<PathBuf> {
for aospkg in [
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../software/coreutils/dist/package.aospkg"),
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../node_modules/@agentos-software/coreutils/dist/package.aospkg"),
] {
if aospkg.is_file() {
return std::fs::canonicalize(aospkg).ok();
}
}
for dir in [
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../software/coreutils/dist/package"),
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../node_modules/@agentos-software/coreutils/dist/package"),
] {
if dir.join("agentos-package.json").is_file() {
return std::fs::canonicalize(dir).ok();
}
}
None
}
#[tokio::test(flavor = "multi_thread")]
async fn pty_shell_round_trip_via_boot_packages() {
if !common::require_sidecar("pty_shell_round_trip_via_boot_packages") {
return;
}
let Some(package_path) = coreutils_package_path() else {
eprintln!("skipping: coreutils package is not built");
return;
};
common::ensure_sidecar_env();
let os = AgentOs::create(AgentOsConfig {
packages: vec![PackageRef {
path: package_path.to_string_lossy().into_owned(),
}],
..Default::default()
})
.await
.expect("create VM with boot packages");
let shell = os
.open_shell(OpenShellOptions {
command: Some(String::from("sh")),
args: vec![
String::from("-c"),
String::from("echo before-read; read line; echo got:$line"),
],
cwd: Some(String::from("/")),
cols: Some(80),
rows: Some(24),
..Default::default()
})
.expect("open_shell");
let (data_tx, mut data_rx) = tokio::sync::mpsc::unbounded_channel();
let _data = os
.on_shell_data(&shell.shell_id, move |event| {
let _ = data_tx.send(event.data);
})
.expect("on_shell_data subscription");
let saw_before = tokio::time::timeout(std::time::Duration::from_secs(20), async {
let mut acc = Vec::<u8>::new();
while let Some(chunk) = data_rx.recv().await {
acc.extend_from_slice(&chunk);
if String::from_utf8_lossy(&acc).contains("before-read") {
return true;
}
}
false
})
.await
.unwrap_or(false);
assert!(saw_before, "guest banner must arrive on the data stream");
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
os.write_shell(
&shell.shell_id,
StdinInput::Text(String::from("marker-input\n")),
)
.expect("write_shell");
let saw_roundtrip = tokio::time::timeout(std::time::Duration::from_secs(20), async {
let mut acc = Vec::<u8>::new();
while let Some(chunk) = data_rx.recv().await {
acc.extend_from_slice(&chunk);
if String::from_utf8_lossy(&acc).contains("got:marker-input") {
return true;
}
}
false
})
.await
.unwrap_or(false);
assert!(
saw_roundtrip,
"write_shell input must reach the guest read() and its output must surface"
);
let exit_code = tokio::time::timeout(
std::time::Duration::from_secs(20),
os.wait_shell(&shell.shell_id),
)
.await
.expect("wait_shell must resolve after the guest exits")
.expect("wait_shell result");
assert_eq!(exit_code, 0, "clean guest exit");
os.shutdown().await.expect("shutdown");
}