Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions libs/cua-driver/rust/crates/cua-driver/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2037,7 +2037,19 @@ pub fn run_call(
.clone()
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
cua_driver_core::tool_args::sanitize_reserved_args(&mut args_for_daemon);
let transport_session = format!("cli-{}", uuid::Uuid::new_v4());
let named_session = args_for_daemon
.get("session")
.and_then(serde_json::Value::as_str)
.is_some_and(|session| !session.is_empty() && session != "default");
// One-shot CLI processes share one daemon-scoped ownership namespace
// for explicit public labels. The daemon adds its runtime prefix, so
// this cannot attach to another daemon generation or transport kind.
// Anonymous calls keep their disposable per-process lease below.
let transport_session = if named_session {
"cli-explicit".to_owned()
} else {
format!("cli-{}", uuid::Uuid::new_v4())
};
let req = crate::serve::DaemonRequest {
method: "call".into(),
name: Some(tool.to_owned()),
Expand All @@ -2050,17 +2062,19 @@ pub fn run_call(
client_kind: Some(cua_driver_core::daemon::DaemonClientKind::Cli),
};
let response = crate::serve::send_request(&socket_path, &req);
let cleanup = crate::serve::DaemonRequest {
method: "session_end".into(),
name: None,
args: None,
session_id: Some(transport_session),
observation_origin: None,
client_kind: Some(cua_driver_core::daemon::DaemonClientKind::Cli),
};
let cleanup_result = crate::serve::send_request(&socket_path, &cleanup);
if let Err(error) = cleanup_result {
eprintln!("warning: disposable session cleanup failed: {error}");
if !named_session {
let cleanup = crate::serve::DaemonRequest {
method: "session_end".into(),
name: None,
args: None,
session_id: Some(transport_session),
observation_origin: None,
client_kind: Some(cua_driver_core::daemon::DaemonClientKind::Cli),
};
let cleanup_result = crate::serve::send_request(&socket_path, &cleanup);
if let Err(error) = cleanup_result {
eprintln!("warning: disposable session cleanup failed: {error}");
}
}
match response {
Ok(resp) => {
Expand Down
124 changes: 124 additions & 0 deletions libs/cua-driver/rust/crates/cua-driver/tests/daemon_required_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,130 @@ fn cli_call_succeeds_through_test_owned_daemon() {
assert!(response.structured().is_object());
}

#[test]
fn named_session_survives_across_one_shot_cli_calls() {
let mut driver = CliDriver::new();
assert!(driver.available(), "test daemon failed to start");
let session = format!("synthetic-cli-lifecycle-{}", std::process::id());

let started = driver.call(
"start_session",
serde_json::json!({"session": session, "capture_scope": "window"}),
);
assert!(!started.is_error(), "start failed: {}", started.text());

let state = driver.call("get_session_state", serde_json::json!({"session": session}));
assert!(
!state.is_error(),
"named session did not survive the next CLI process: {}",
state.text()
);
assert_eq!(state.structured()["session"], session);

let sessions = Command::new(env!("CARGO_BIN_EXE_cua-driver"))
.args([
"sessions",
"list",
"--json",
"--socket",
driver.daemon_socket().expect("test daemon socket"),
])
.output()
.expect("list live sessions");
assert!(
sessions.status.success(),
"session list failed: {}",
String::from_utf8_lossy(&sessions.stderr)
);
let sessions: serde_json::Value =
serde_json::from_slice(&sessions.stdout).expect("session list JSON");
assert_eq!(sessions["count"], 1);

let ended = driver.call("end_session", serde_json::json!({"session": session}));
assert!(!ended.is_error(), "end failed: {}", ended.text());

let sessions = Command::new(env!("CARGO_BIN_EXE_cua-driver"))
.args([
"sessions",
"list",
"--json",
"--socket",
driver.daemon_socket().expect("test daemon socket"),
])
.output()
.expect("list ended sessions");
assert!(
sessions.status.success(),
"session list failed: {}",
String::from_utf8_lossy(&sessions.stderr)
);
let sessions: serde_json::Value =
serde_json::from_slice(&sessions.stdout).expect("session list JSON");
assert_eq!(sessions["count"], 0);
}

#[test]
fn implicitly_started_named_session_survives_across_one_shot_cli_calls() {
let mut driver = CliDriver::new();
assert!(driver.available(), "test daemon failed to start");
let session = format!("synthetic-cli-implicit-{}", std::process::id());

let first_action = driver.call("get_config", serde_json::json!({"session": session}));
assert!(
!first_action.is_error(),
"implicit first action failed: {}",
first_action.text()
);

let state = driver.call("get_session_state", serde_json::json!({"session": session}));
assert!(
!state.is_error(),
"implicitly started session did not survive the next CLI process: {}",
state.text()
);
assert_eq!(state.structured()["session"], session);

let ended = driver.call("end_session", serde_json::json!({"session": session}));
assert!(!ended.is_error(), "end failed: {}", ended.text());
}

#[test]
fn named_cli_session_cleanup_is_isolated() {
let mut driver = CliDriver::new();
assert!(driver.available(), "test daemon failed to start");
let first = format!("synthetic-cli-isolation-a-{}", std::process::id());
let second = format!("synthetic-cli-isolation-b-{}", std::process::id());

for session in [&first, &second] {
let started = driver.call(
"start_session",
serde_json::json!({"session": session, "capture_scope": "window"}),
);
assert!(!started.is_error(), "start failed: {}", started.text());
}

let ended = driver.call("end_session", serde_json::json!({"session": first}));
assert!(!ended.is_error(), "first end failed: {}", ended.text());

let anonymous = driver.call("get_config", serde_json::json!({}));
assert!(
!anonymous.is_error(),
"anonymous one-shot call failed: {}",
anonymous.text()
);

let state = driver.call("get_session_state", serde_json::json!({"session": second}));
assert!(
!state.is_error(),
"ending another named session or cleaning an anonymous call ended the survivor: {}",
state.text()
);
assert_eq!(state.structured()["session"], second);

let ended = driver.call("end_session", serde_json::json!({"session": second}));
assert!(!ended.is_error(), "second end failed: {}", ended.text());
}

#[test]
fn revoke_cli_ends_the_exact_live_session() {
let mut driver = CliDriver::new();
Expand Down
Loading