use std::collections::VecDeque; use std::convert::Infallible; use std::ffi::OsString; use std::io::Write; use std::os::unix::ffi::OsStringExt; use std::path::PathBuf; use lazaret_client::{ BootstrapAcquisition, ClientBackend, ClientCommand, ClientError, DomainStatus, run, }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Endpoint { Existing, Started, Published, } #[derive(Default)] struct FakeBackend { observations: VecDeque>, acquisition: Option, bootstrap: Option>>, waited: Option>>, execution: Option>>, actions: Vec<&'static str>, validation: Option>, } impl ClientBackend for FakeBackend { type Endpoint = Endpoint; type Error = Infallible; fn validate(&mut self, _command: &ClientCommand) -> Result<(), ClientError> { self.actions.push("validate"); self.validation.take().map_or(Ok(()), Err) } fn observe(&mut self) -> Result, Self::Error> { self.actions.push("observe"); Ok(self.observations.pop_front().unwrap()) } fn acquire_bootstrap(&mut self) -> Result { self.actions.push("acquire"); Ok(self.acquisition.unwrap()) } fn bootstrap(&mut self) -> Result> { self.actions.push("bootstrap"); self.bootstrap.take().unwrap() } fn wait_until_ready(&mut self) -> Result> { self.actions.push("wait"); self.waited.take().unwrap() } fn execute( &mut self, _endpoint: Self::Endpoint, _argv: &[Vec], stdout: &mut dyn Write, stderr: &mut dyn Write, ) -> Result> { self.actions.push("execute"); stdout.write_all(b"out").unwrap(); stderr.write_all(b"err").unwrap(); self.execution.take().unwrap_or(Ok(0)) } } fn command() -> ClientCommand { ClientCommand { profile: "agent".into(), workspace: PathBuf::from("/workspace"), configuration: None, argv: vec![b"sh".to_vec(), b"-c".to_vec(), b"exit 0".to_vec()], } } fn invoke(backend: &mut FakeBackend) -> Result<(i32, Vec, Vec), String> { let mut stdout = Vec::new(); let mut stderr = Vec::new(); run(backend, &command(), &mut stdout, &mut stderr) .map(|status| (status, stdout, stderr)) .map_err(|error| error.to_string()) } #[test] fn cold_start_leader_bootstraps_before_forwarding_streams() { let mut backend = FakeBackend { observations: [DomainStatus::Absent].into(), acquisition: Some(BootstrapAcquisition::Leader), bootstrap: Some(Ok(Endpoint::Started)), ..FakeBackend::default() }; assert_eq!( invoke(&mut backend), Ok((0, b"out".to_vec(), b"err".to_vec())) ); assert_eq!( backend.actions, ["validate", "observe", "acquire", "bootstrap", "execute"] ); } #[test] fn ready_domain_is_joined_without_bootstrap() { let mut backend = FakeBackend { observations: [DomainStatus::Ready(Endpoint::Existing)].into(), ..FakeBackend::default() }; assert_eq!(invoke(&mut backend).unwrap().0, 0); assert_eq!(backend.actions, ["validate", "observe", "execute"]); } #[test] fn bootstrap_owner_disconnect_fails_that_closed_session() { let mut backend = FakeBackend { observations: [DomainStatus::Absent].into(), acquisition: Some(BootstrapAcquisition::Leader), bootstrap: Some(Err(ClientError::BootstrapOwnerDisconnected)), ..FakeBackend::default() }; assert!( invoke(&mut backend) .unwrap_err() .contains("bootstrap owner disconnected") ); assert_eq!( backend.actions, ["validate", "observe", "acquire", "bootstrap"] ); } #[test] fn distinct_waiter_joins_after_publication() { let mut backend = FakeBackend { observations: [DomainStatus::Starting].into(), waited: Some(Ok(Endpoint::Published)), ..FakeBackend::default() }; assert_eq!(invoke(&mut backend).unwrap().0, 0); assert_eq!(backend.actions, ["validate", "observe", "wait", "execute"]); } #[test] fn distinct_waiter_is_not_restarted_after_owner_loss() { let mut backend = FakeBackend { observations: [DomainStatus::Starting].into(), waited: Some(Err(ClientError::WaitingDomainFailed)), ..FakeBackend::default() }; assert!( invoke(&mut backend) .unwrap_err() .contains("failed while this Client was waiting") ); assert_eq!(backend.actions, ["validate", "observe", "wait"]); } #[test] fn nonzero_guest_exit_is_the_cli_status() { let mut backend = FakeBackend { observations: [DomainStatus::Ready(Endpoint::Existing)].into(), execution: Some(Ok(23)), ..FakeBackend::default() }; assert_eq!(invoke(&mut backend).unwrap().0, 23); } #[test] fn stale_domain_enters_lock_arbitration_while_live_stale_and_lost_faces_refuse() { let mut unowned = FakeBackend { observations: [DomainStatus::Stale].into(), acquisition: Some(BootstrapAcquisition::Leader), bootstrap: Some(Ok(Endpoint::Published)), ..FakeBackend::default() }; assert_eq!(invoke(&mut unowned).unwrap().0, 0); assert_eq!( unowned.actions, ["validate", "observe", "acquire", "bootstrap", "execute"] ); let mut live = FakeBackend { observations: [DomainStatus::Stale].into(), acquisition: Some(BootstrapAcquisition::Waiter), waited: Some(Err(ClientError::StaleCoordinator)), ..FakeBackend::default() }; assert!(invoke(&mut live).unwrap_err().contains("stale")); assert_eq!(live.actions, ["validate", "observe", "acquire", "wait"]); let mut lost = FakeBackend { observations: [DomainStatus::Ready(Endpoint::Existing)].into(), execution: Some(Err(ClientError::LostConnection)), ..FakeBackend::default() }; assert!( invoke(&mut lost) .unwrap_err() .contains("connection was lost") ); } #[test] fn unsupported_host_and_configuration_errors_keep_their_cause() { for (error, expected) in [ ( ClientError::UnsupportedHost("Linux kernel 6.8 is below the required 6.9 floor".into()), "host is unsupported: Linux kernel 6.8 is below the required 6.9 floor", ), ( ClientError::Configuration("profile `agent` is not configured".into()), "configuration is invalid: profile `agent` is not configured", ), ] { let mut backend = FakeBackend { validation: Some(error), ..FakeBackend::default() }; assert_eq!(invoke(&mut backend).unwrap_err(), expected); assert_eq!(backend.actions, ["validate"]); } } #[test] fn command_line_accepts_profile_entry_and_preserves_explicit_command_bytes() { let parsed = lazaret_client::cli::parse(["agent"].map(OsString::from), PathBuf::from("/current")) .unwrap(); let lazaret_client::cli::Command::Run(parsed) = parsed else { panic!("expected run command"); }; assert_eq!(parsed.profile, "agent"); assert_eq!(parsed.workspace, PathBuf::from("/current")); assert_eq!(parsed.configuration, None); assert_eq!(parsed.argv, [b"/bin/bash".to_vec()]); let parsed = lazaret_client::cli::parse( ["run", "--profile", "state"].map(OsString::from), PathBuf::from("/current"), ) .unwrap(); let lazaret_client::cli::Command::Run(parsed) = parsed else { panic!("expected run command"); }; assert_eq!(parsed.profile, "state"); assert_eq!(parsed.argv, [b"/bin/bash".to_vec()]); let parsed = lazaret_client::cli::parse( [ "agent", "--workspace", "/project", "--config", "/host/config.toml", "--", "tool", "argument", ] .map(OsString::from), PathBuf::from("/current"), ) .unwrap(); let lazaret_client::cli::Command::Run(parsed) = parsed else { panic!("expected run command"); }; assert_eq!(parsed.profile, "agent"); assert_eq!(parsed.workspace, PathBuf::from("/project")); assert_eq!( parsed.configuration, Some(PathBuf::from("/host/config.toml")) ); assert_eq!(parsed.argv, [b"tool".to_vec(), b"argument".to_vec()]); assert!( lazaret_client::cli::parse( ["run", "--", "tool"].map(OsString::from), PathBuf::from("/current") ) .unwrap_err() .to_string() .contains("profile") ); } #[test] fn command_line_preserves_non_utf8_paths_and_command_arguments() { let workspace = OsString::from_vec(b"/project-\xff".to_vec()); let argument = OsString::from_vec(b"argument-\xfe".to_vec()); let parsed = lazaret_client::cli::parse( [ OsString::from("run"), OsString::from("--profile"), OsString::from("agent"), OsString::from("--workspace"), workspace.clone(), OsString::from("--"), OsString::from("tool"), argument, ], PathBuf::from("/current"), ) .unwrap(); let lazaret_client::cli::Command::Run(parsed) = parsed else { panic!("expected run command"); }; assert_eq!(parsed.workspace, PathBuf::from(workspace)); assert_eq!(parsed.argv, [b"tool".to_vec(), b"argument-\xfe".to_vec()]); } #[test] fn public_help_explains_commands_and_options() { for (arguments, expected) in [ ( &["--help"][..], &[ "Run development tools in an isolated gVisor sandbox", "Usage: lazaret ", "lazaret PROFILE", "run", "state", ][..], ), ( &["run", "--help"], &[ "Agent profile to use", "Project directory to share", "default `/bin/bash`", ], ), ( &["state", "--help"], &["list", "inspect", "verify", "reset"], ), ( &["state", "reset", "--help"], &[ "Reset every configured persistent sandbox path", "Reset one exact configured container path", "interactive confirmation", ], ), ] { let error = lazaret_client::cli::parse( arguments.iter().map(OsString::from), PathBuf::from("/current"), ) .unwrap_err(); assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp); let help = error.to_string(); for text in expected { assert!( help.contains(text), "{arguments:?} did not explain {text:?}" ); } } } #[test] fn clap_rejects_missing_profile_and_ambiguous_reset_scope() { let workspace = PathBuf::from("/current"); for arguments in [ vec!["run", "--", "tool"], vec!["state", "reset", "--profile", "agent"], vec![ "state", "reset", "--profile", "agent", "--all", "--path", "/usr", ], ] { let error = lazaret_client::cli::parse( arguments.into_iter().map(OsString::from), workspace.clone(), ) .unwrap_err(); assert_eq!(error.exit_code(), 2); } }