1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Shell / PTY e2e against a real `agentos-sidecar`.
//!
//! `open_shell` spawns a PTY-backed `sh` (a WASM command). This suite fails fast by default when
//! that command is unavailable; set `AGENT_OS_CLIENT_ALLOW_E2E_SKIPS=1` only for local skip-only
//! runs.
//!
//! When the shell IS available the suite asserts the real TS contract: open returns a synthetic
//! `shell-N` id (NOT a pid), `on_shell_data` carries stdout, `write_shell` reaches the shell,
//! `resize_shell` validates existence, and `close_shell` plus the ShellNotFound error contract hold.
mod common;
use agentos_client::{ClientError, OpenShellOptions, StdinInput};
use futures::StreamExt;
#[tokio::test]
async fn shell_surface_open_write_data_resize_close() {
if !common::require_sidecar("shell_surface_open_write_data_resize_close") {
return;
}
let os = common::new_vm_with_wasm_commands().await;
// --- Runtime-independent ShellNotFound contract (no WASM needed) ------------------------------
// Every shell operation on an unknown id returns ShellNotFound, asserted against the real sidecar
// regardless of whether a PTY-backed WASM shell is available.
assert!(
matches!(
os.write_shell("shell-missing", StdinInput::Text("x".to_string())),
Err(ClientError::ShellNotFound(_))
),
"write_shell(unknown) must return ShellNotFound"
);
assert!(
matches!(
os.resize_shell("shell-missing", 80, 24),
Err(ClientError::ShellNotFound(_))
),
"resize_shell(unknown) must return ShellNotFound"
);
assert!(
matches!(
os.close_shell("shell-missing"),
Err(ClientError::ShellNotFound(_))
),
"close_shell(unknown) must return ShellNotFound"
);
assert!(
matches!(
os.on_shell_data("shell-missing"),
Err(ClientError::ShellNotFound(_))
),
"on_shell_data(unknown) must return ShellNotFound"
);
if !common::require_wasm_commands(&os, "shell_surface_open_write_data_resize_close").await {
os.shutdown().await.expect("shutdown after local skip");
return;
}
// --- open_shell: synthetic id, NOT a pid ------------------------------------------------------
let shell = os
.open_shell(OpenShellOptions {
cols: Some(80),
rows: Some(24),
..Default::default()
})
.expect("open_shell");
assert!(
shell.shell_id.starts_with("shell-"),
"open_shell must return a synthetic shell-N id (not a pid), got {}",
shell.shell_id
);
// --- on_shell_data: subscribe to stdout (stderr is on a separate channel) ---------------------
let mut data = os
.on_shell_data(&shell.shell_id)
.expect("on_shell_data for live shell");
// A separate stderr channel must also be subscribable.
let _stderr = os
.on_shell_stderr(&shell.shell_id)
.expect("on_shell_stderr for live shell");
// --- write_shell: drive the shell, expect the echoed command/output on the data stream --------
// A PTY shell echoes typed input and runs the command. `echo shell-marker` produces the literal
// marker on stdout. We scan the data stream for the marker rather than asserting an exact frame,
// because PTY line-discipline echo + prompts interleave.
os.write_shell(
&shell.shell_id,
StdinInput::Text("echo shell-marker\n".to_string()),
)
.expect("write_shell");
let saw_marker = tokio::time::timeout(std::time::Duration::from_secs(10), async {
let mut acc = Vec::<u8>::new();
while let Some(chunk) = data.next().await {
acc.extend_from_slice(&chunk);
if String::from_utf8_lossy(&acc).contains("shell-marker") {
return true;
}
}
false
})
.await
.unwrap_or(false);
assert!(
saw_marker,
"the shell's data stream should surface the echoed `shell-marker` output"
);
// --- resize_shell: validates existence (no native winsize op, so it is a best-effort no-op) ----
os.resize_shell(&shell.shell_id, 120, 40)
.expect("resize_shell on a live shell must succeed");
// --- close_shell: removes the entry; subsequent shell calls report ShellNotFound --------------
os.close_shell(&shell.shell_id).expect("close_shell");
let err = os
.write_shell(&shell.shell_id, StdinInput::Text("x".to_string()))
.expect_err("write to a closed shell must error");
assert!(
matches!(err, ClientError::ShellNotFound(id) if id == shell.shell_id),
"closed shell must report ShellNotFound"
);
// --- ShellNotFound contract for a never-opened id ---------------------------------------------
match os.on_shell_data("shell-does-not-exist") {
Err(ClientError::ShellNotFound(_)) => {}
Ok(_) => panic!("unknown shell id must error"),
Err(other) => panic!("expected ShellNotFound, got {other:?}"),
}
os.shutdown().await.expect("shutdown");
}