- expose command-monitor conversations and preserve visible agent transcripts - add bounded polling and a dedicated shell interrupt tool - improve direct-provider images, skills, tool history, and usage handling - package and brand Galaxy Control across releases, installers, persistence, and docs
336 lines
11 KiB
Rust
336 lines
11 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
|
|
#[cfg(unix)]
|
|
use command::blocking::Command;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn control_endpoint_composes_loopback_control_route() {
|
|
assert_eq!(
|
|
ControlEndpoint::localhost(4000).url(),
|
|
"http://127.0.0.1:4000/v1/control"
|
|
);
|
|
}
|
|
#[test]
|
|
fn broker_socket_reference_is_bound_to_instance_identity() {
|
|
let record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
|
|
assert_eq!(
|
|
record
|
|
.credential_broker
|
|
.expect("credential broker")
|
|
.socket_path,
|
|
PathBuf::from(format!("{}.broker.sock", record.instance_id.0))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn registered_instance_round_trips_discovery_record() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
let _registered = RegisteredInstance::register_in_dir_for_test(record.clone(), dir.path())
|
|
.expect("registered");
|
|
let records = list_instances_from_dir(dir.path(), "local");
|
|
assert_eq!(records, vec![record]);
|
|
}
|
|
|
|
#[test]
|
|
fn incompatible_protocol_record_is_ignored() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let mut record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
record.protocol_version = PROTOCOL_VERSION + 1;
|
|
let _registered =
|
|
RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered");
|
|
|
|
assert!(list_instances_from_dir(dir.path(), "local").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_record_and_matching_broker_socket_are_pruned() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let record_path = dir.path().join("inst_malformed.json");
|
|
let socket_path = dir.path().join("inst_malformed.broker.sock");
|
|
fs::write(&record_path, "not json").expect("write malformed record");
|
|
fs::write(&socket_path, "").expect("write broker socket");
|
|
|
|
assert!(list_instances_from_dir(dir.path(), "local").is_empty());
|
|
assert!(!record_path.exists());
|
|
assert!(!socket_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn orphan_broker_sockets_are_pruned_after_grace_period() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let orphan_path = dir.path().join("inst_orphan.broker.sock");
|
|
let retained_filename = PathBuf::from("inst_retained.broker.sock");
|
|
let retained_path = dir.path().join(&retained_filename);
|
|
fs::write(&orphan_path, "").expect("write orphan socket");
|
|
fs::write(&retained_path, "").expect("write retained socket");
|
|
|
|
sweep_orphan_broker_sockets(
|
|
dir.path(),
|
|
&HashSet::from([retained_filename]),
|
|
Duration::ZERO,
|
|
);
|
|
|
|
assert!(!orphan_path.exists());
|
|
assert!(retained_path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn abandoned_temp_records_are_pruned_after_grace_period() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let temp_path = dir.path().join("inst_abandoned.json.tmp");
|
|
fs::write(&temp_path, "").expect("write temporary record");
|
|
|
|
sweep_abandoned_temp_records(dir.path(), Duration::ZERO);
|
|
|
|
assert!(!temp_path.exists());
|
|
}
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn stale_process_record_is_pruned() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let mut child = Command::new("true")
|
|
.spawn()
|
|
.expect("short-lived process starts");
|
|
let pid = child.id();
|
|
child.wait().expect("short-lived process exits");
|
|
let mut record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
record.pid = pid;
|
|
let socket_path = dir.path().join(broker_socket_filename(&record.instance_id));
|
|
fs::write(&socket_path, "").expect("write broker socket");
|
|
let registered =
|
|
RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered");
|
|
|
|
assert!(list_instances_from_dir(dir.path(), "local").is_empty());
|
|
assert!(!registered.path.exists());
|
|
assert!(!socket_path.exists());
|
|
}
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn multiple_live_process_records_are_discovered() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let mut first_process = Command::new("sleep")
|
|
.arg("10")
|
|
.spawn()
|
|
.expect("first process starts");
|
|
let mut second_process = Command::new("sleep")
|
|
.arg("10")
|
|
.spawn()
|
|
.expect("second process starts");
|
|
let mut first_record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
first_record.pid = first_process.id();
|
|
let mut second_record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4001)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
second_record.pid = second_process.id();
|
|
let first_id = first_record.instance_id.clone();
|
|
let second_id = second_record.instance_id.clone();
|
|
let _first = RegisteredInstance::register_in_dir_for_test(first_record, dir.path())
|
|
.expect("first registered");
|
|
let _second = RegisteredInstance::register_in_dir_for_test(second_record, dir.path())
|
|
.expect("second registered");
|
|
|
|
let ids = list_instances_from_dir(dir.path(), "local")
|
|
.into_iter()
|
|
.map(|record| record.instance_id)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(ids.len(), 2);
|
|
assert!(ids.contains(&first_id));
|
|
assert!(ids.contains(&second_id));
|
|
|
|
first_process.kill().expect("first process stops");
|
|
first_process.wait().expect("first process reaped");
|
|
second_process.kill().expect("second process stops");
|
|
second_process.wait().expect("second process reaped");
|
|
}
|
|
|
|
#[test]
|
|
fn records_from_other_channels_are_ignored() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"dev",
|
|
"dev.galaxy.Galaxy-Dev",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
let socket_path = dir.path().join(broker_socket_filename(&record.instance_id));
|
|
fs::write(&socket_path, "").expect("write broker socket");
|
|
let _registered =
|
|
RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered");
|
|
|
|
assert!(list_instances_from_dir(dir.path(), "local").is_empty());
|
|
assert!(socket_path.exists());
|
|
}
|
|
#[test]
|
|
fn serialized_discovery_record_does_not_contain_raw_credential_material() {
|
|
let raw_secret = "raw-secret-token-material";
|
|
let record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
let serialized = serde_json::to_string_pretty(&record).expect("serialize");
|
|
assert!(!serialized.contains(raw_secret));
|
|
assert!(!serialized.contains("auth_token"));
|
|
assert!(!serialized.contains("bearer_token"));
|
|
}
|
|
|
|
#[test]
|
|
fn disabled_record_does_not_expose_actionable_authority() {
|
|
let record = InstanceRecord::for_current_process(
|
|
None,
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
assert!(record.endpoint.is_none());
|
|
assert!(record.credential_broker.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unsafe_or_divergent_discovery_authority() {
|
|
let mut record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
record
|
|
.validate_local_control_authority()
|
|
.expect("matching 127.0.0.1 endpoints are accepted");
|
|
|
|
record.endpoint.as_mut().expect("endpoint").host = "localhost".to_owned();
|
|
let err = record
|
|
.validate_local_control_authority()
|
|
.expect_err("localhost alias is rejected");
|
|
assert_eq!(err.code, ErrorCode::UnauthorizedLocalClient);
|
|
|
|
record.endpoint = Some(ControlEndpoint::localhost(4000));
|
|
record
|
|
.credential_broker
|
|
.as_mut()
|
|
.expect("credential broker")
|
|
.socket_path = "different.broker.sock".into();
|
|
let err = record
|
|
.validate_local_control_authority()
|
|
.expect_err("divergent broker socket is rejected");
|
|
assert_eq!(err.code, ErrorCode::UnauthorizedLocalClient);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn discovery_directory_is_owner_only_on_unix() {
|
|
use std::os::unix::fs::PermissionsExt as _;
|
|
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
let _registered =
|
|
RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered");
|
|
let mode = fs::metadata(dir.path())
|
|
.expect("metadata")
|
|
.permissions()
|
|
.mode()
|
|
& 0o777;
|
|
assert_eq!(mode, 0o700);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn discovery_record_is_owner_only_on_unix() {
|
|
let dir = tempfile::tempdir().expect("temp dir");
|
|
let record = InstanceRecord::for_current_process(
|
|
Some(ControlEndpoint::localhost(4000)),
|
|
"local",
|
|
"dev.galaxy.GalaxyLocal",
|
|
Some("test".to_owned()),
|
|
crate::protocol::ActionKind::implemented_metadata(),
|
|
);
|
|
let registered =
|
|
RegisteredInstance::register_in_dir_for_test(record, dir.path()).expect("registered");
|
|
let mode = fs::metadata(®istered.path)
|
|
.expect("metadata")
|
|
.permissions()
|
|
.mode()
|
|
& 0o777;
|
|
assert_eq!(mode, 0o600);
|
|
}
|
|
|
|
impl RegisteredInstance {
|
|
fn register_in_dir_for_test(record: InstanceRecord, dir: &Path) -> Result<Self, ControlError> {
|
|
fs::create_dir_all(dir).expect("create dir");
|
|
#[cfg(unix)]
|
|
set_private_dir_permissions(dir)?;
|
|
let path = record_path(dir, &record.instance_id);
|
|
let bytes = serde_json::to_vec_pretty(&record).map_err(|err| {
|
|
ControlError::with_details(
|
|
ErrorCode::Internal,
|
|
"failed to serialize local-control discovery test record",
|
|
err.to_string(),
|
|
)
|
|
})?;
|
|
fs::write(&path, bytes).map_err(|err| {
|
|
ControlError::with_details(
|
|
ErrorCode::Internal,
|
|
"failed to write local-control discovery test record",
|
|
err.to_string(),
|
|
)
|
|
})?;
|
|
#[cfg(unix)]
|
|
set_private_permissions(&path)?;
|
|
Ok(Self {
|
|
record,
|
|
path,
|
|
broker_socket_path: None,
|
|
})
|
|
}
|
|
}
|