mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 08:03:46 +00:00
Fix desktop runtime startup cleanup
This commit is contained in:
parent
5917ec72fe
commit
406f55dc52
7 changed files with 232 additions and 134 deletions
|
|
@ -97,6 +97,11 @@
|
||||||
- This triggers `examples/shared/Dockerfile.dev` which builds the server binary from local source and packages it into the Docker image.
|
- This triggers `examples/shared/Dockerfile.dev` which builds the server binary from local source and packages it into the Docker image.
|
||||||
- Example: `SANDBOX_AGENT_DEV=1 pnpm --filter @sandbox-agent/example-mcp start`
|
- Example: `SANDBOX_AGENT_DEV=1 pnpm --filter @sandbox-agent/example-mcp start`
|
||||||
|
|
||||||
|
## Docker Test Image
|
||||||
|
|
||||||
|
- Docker-backed Rust and TypeScript tests build `docker/test-agent/Dockerfile` directly in-process and cache the image tag only in memory (`OnceLock` in Rust, module-level variable in TypeScript).
|
||||||
|
- Do not add cross-process image-build scripts unless there is a concrete need for them.
|
||||||
|
|
||||||
## Install Version References
|
## Install Version References
|
||||||
|
|
||||||
- Channel policy:
|
- Channel policy:
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
IMAGE_TAG="${SANDBOX_AGENT_TEST_IMAGE:-sandbox-agent-test:dev}"
|
|
||||||
LOCK_DIR="$ROOT_DIR/.context/docker-test-image.lock"
|
|
||||||
STAMP_FILE="$ROOT_DIR/.context/docker-test-image.stamp"
|
|
||||||
|
|
||||||
INPUTS=(
|
|
||||||
"$ROOT_DIR/Cargo.toml"
|
|
||||||
"$ROOT_DIR/Cargo.lock"
|
|
||||||
"$ROOT_DIR/server"
|
|
||||||
"$ROOT_DIR/gigacode"
|
|
||||||
"$ROOT_DIR/resources/agent-schemas/artifacts"
|
|
||||||
"$ROOT_DIR/scripts/agent-configs"
|
|
||||||
"$ROOT_DIR/docker/test-agent/Dockerfile"
|
|
||||||
)
|
|
||||||
|
|
||||||
release_lock() {
|
|
||||||
if [[ -d "$LOCK_DIR" ]]; then
|
|
||||||
rm -rf "$LOCK_DIR"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
latest_input_mtime() {
|
|
||||||
find "${INPUTS[@]}" -type f -exec stat -f '%m' {} + | sort -nr | head -n1
|
|
||||||
}
|
|
||||||
|
|
||||||
image_is_ready() {
|
|
||||||
if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$STAMP_FILE" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local stamp_mtime
|
|
||||||
stamp_mtime="$(stat -f '%m' "$STAMP_FILE")"
|
|
||||||
local latest_mtime
|
|
||||||
latest_mtime="$(latest_input_mtime)"
|
|
||||||
|
|
||||||
[[ -n "$latest_mtime" && "$stamp_mtime" -ge "$latest_mtime" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p "$ROOT_DIR/.context"
|
|
||||||
|
|
||||||
if image_is_ready; then
|
|
||||||
printf '%s\n' "$IMAGE_TAG"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
trap release_lock EXIT
|
|
||||||
|
|
||||||
if image_is_ready; then
|
|
||||||
printf '%s\n' "$IMAGE_TAG"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
--tag "$IMAGE_TAG" \
|
|
||||||
--file "$ROOT_DIR/docker/test-agent/Dockerfile" \
|
|
||||||
"$ROOT_DIR" \
|
|
||||||
>/dev/null
|
|
||||||
|
|
||||||
touch "$STAMP_FILE"
|
|
||||||
|
|
||||||
printf '%s\n' "$IMAGE_TAG"
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const REPO_ROOT = resolve(__dirname, "../../../..");
|
const REPO_ROOT = resolve(__dirname, "../../../..");
|
||||||
const ENSURE_IMAGE = resolve(REPO_ROOT, "scripts/test-rig/ensure-image.sh");
|
|
||||||
const CONTAINER_PORT = 3000;
|
const CONTAINER_PORT = 3000;
|
||||||
const DEFAULT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
const DEFAULT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||||
|
const DEFAULT_IMAGE_TAG = "sandbox-agent-test:dev";
|
||||||
const STANDARD_PATHS = new Set([
|
const STANDARD_PATHS = new Set([
|
||||||
"/usr/local/sbin",
|
"/usr/local/sbin",
|
||||||
"/usr/local/bin",
|
"/usr/local/bin",
|
||||||
|
|
@ -184,11 +184,22 @@ function ensureImage(): string {
|
||||||
return cachedImage;
|
return cachedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedImage = execFileSync("bash", [ENSURE_IMAGE], {
|
cachedImage = process.env.SANDBOX_AGENT_TEST_IMAGE ?? DEFAULT_IMAGE_TAG;
|
||||||
cwd: REPO_ROOT,
|
execFileSync(
|
||||||
encoding: "utf8",
|
"docker",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
[
|
||||||
}).trim();
|
"build",
|
||||||
|
"--tag",
|
||||||
|
cachedImage,
|
||||||
|
"--file",
|
||||||
|
resolve(REPO_ROOT, "docker/test-agent/Dockerfile"),
|
||||||
|
REPO_ROOT,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: REPO_ROOT,
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
},
|
||||||
|
);
|
||||||
return cachedImage;
|
return cachedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl DesktopProblem {
|
||||||
install_command: Option<String>,
|
install_command: Option<String>,
|
||||||
processes: Vec<DesktopProcessInfo>,
|
processes: Vec<DesktopProcessInfo>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let message = if missing_dependencies.is_empty() {
|
let mut message = if missing_dependencies.is_empty() {
|
||||||
"Desktop dependencies are not installed".to_string()
|
"Desktop dependencies are not installed".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
|
|
@ -37,6 +37,11 @@ impl DesktopProblem {
|
||||||
missing_dependencies.join(", ")
|
missing_dependencies.join(", ")
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
if let Some(command) = install_command.as_ref() {
|
||||||
|
message.push_str(&format!(
|
||||||
|
". Run `{command}` to install them, or install the required tools manually."
|
||||||
|
));
|
||||||
|
}
|
||||||
Self::new(
|
Self::new(
|
||||||
503,
|
503,
|
||||||
"Desktop Dependencies Missing",
|
"Desktop Dependencies Missing",
|
||||||
|
|
@ -186,3 +191,27 @@ impl DesktopProblem {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dependencies_missing_detail_includes_install_command() {
|
||||||
|
let problem = DesktopProblem::dependencies_missing(
|
||||||
|
vec!["Xvfb".to_string(), "openbox".to_string()],
|
||||||
|
Some("sandbox-agent install desktop --yes".to_string()),
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
let details = problem.to_problem_details();
|
||||||
|
let detail = details.detail.expect("detail");
|
||||||
|
assert!(detail.contains("Desktop dependencies are not installed: Xvfb, openbox"));
|
||||||
|
assert!(detail.contains("sandbox-agent install desktop --yes"));
|
||||||
|
assert_eq!(
|
||||||
|
details.extensions.get("installCommand"),
|
||||||
|
Some(&Value::String(
|
||||||
|
"sandbox-agent install desktop --yes".to_string()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ use std::process::Command as ProcessCommand;
|
||||||
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
|
|
||||||
|
const AUTOMATIC_INSTALL_SUPPORTED_DISTROS: &str =
|
||||||
|
"Automatic desktop dependency installation is supported on Debian/Ubuntu (apt), Fedora/RHEL (dnf), and Alpine (apk).";
|
||||||
|
const AUTOMATIC_INSTALL_UNSUPPORTED_ENVS: &str =
|
||||||
|
"Automatic installation is not supported on macOS, Windows, or Linux distributions without apt, dnf, or apk.";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
pub enum DesktopPackageManager {
|
pub enum DesktopPackageManager {
|
||||||
Apt,
|
Apt,
|
||||||
|
|
@ -20,17 +25,29 @@ pub struct DesktopInstallRequest {
|
||||||
pub no_fonts: bool,
|
pub no_fonts: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn desktop_platform_support_message() -> String {
|
||||||
|
format!("Desktop APIs are only supported on Linux. {AUTOMATIC_INSTALL_SUPPORTED_DISTROS}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linux_install_support_message() -> String {
|
||||||
|
format!("{AUTOMATIC_INSTALL_SUPPORTED_DISTROS} {AUTOMATIC_INSTALL_UNSUPPORTED_ENVS}")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn install_desktop(request: DesktopInstallRequest) -> Result<(), String> {
|
pub fn install_desktop(request: DesktopInstallRequest) -> Result<(), String> {
|
||||||
if std::env::consts::OS != "linux" {
|
if std::env::consts::OS != "linux" {
|
||||||
return Err(
|
return Err(format!(
|
||||||
"desktop installation is only supported on Linux hosts and sandboxes".to_string(),
|
"desktop installation is only supported on Linux. {}",
|
||||||
);
|
linux_install_support_message()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let package_manager = match request.package_manager {
|
let package_manager = match request.package_manager {
|
||||||
Some(value) => value,
|
Some(value) => value,
|
||||||
None => detect_package_manager().ok_or_else(|| {
|
None => detect_package_manager().ok_or_else(|| {
|
||||||
"could not detect a supported package manager (expected apt, dnf, or apk)".to_string()
|
format!(
|
||||||
|
"could not detect a supported package manager. {} Install the desktop dependencies manually on this distribution.",
|
||||||
|
linux_install_support_message()
|
||||||
|
)
|
||||||
})?,
|
})?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -268,6 +285,26 @@ impl fmt::Display for DesktopPackageManager {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_platform_support_message_mentions_linux_and_supported_distros() {
|
||||||
|
let message = desktop_platform_support_message();
|
||||||
|
assert!(message.contains("only supported on Linux"));
|
||||||
|
assert!(message.contains("Debian/Ubuntu (apt)"));
|
||||||
|
assert!(message.contains("Fedora/RHEL (dnf)"));
|
||||||
|
assert!(message.contains("Alpine (apk)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linux_install_support_message_mentions_unsupported_environments() {
|
||||||
|
let message = linux_install_support_message();
|
||||||
|
assert!(message.contains("Debian/Ubuntu (apt)"));
|
||||||
|
assert!(message.contains("Fedora/RHEL (dnf)"));
|
||||||
|
assert!(message.contains("Alpine (apk)"));
|
||||||
|
assert!(message.contains("macOS"));
|
||||||
|
assert!(message.contains("Windows"));
|
||||||
|
assert!(message.contains("without apt, dnf, or apk"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn desktop_packages_support_no_fonts() {
|
fn desktop_packages_support_no_fonts() {
|
||||||
let packages = desktop_packages(DesktopPackageManager::Apt, true);
|
let packages = desktop_packages(DesktopPackageManager::Apt, true);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use tokio::process::{Child, Command};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::desktop_errors::DesktopProblem;
|
use crate::desktop_errors::DesktopProblem;
|
||||||
|
use crate::desktop_install::desktop_platform_support_message;
|
||||||
use crate::desktop_types::{
|
use crate::desktop_types::{
|
||||||
DesktopActionResponse, DesktopDisplayInfoResponse, DesktopErrorInfo,
|
DesktopActionResponse, DesktopDisplayInfoResponse, DesktopErrorInfo,
|
||||||
DesktopKeyboardPressRequest, DesktopKeyboardTypeRequest, DesktopMouseButton,
|
DesktopKeyboardPressRequest, DesktopKeyboardTypeRequest, DesktopMouseButton,
|
||||||
|
|
@ -138,9 +139,7 @@ impl DesktopRuntime {
|
||||||
let mut state = self.inner.lock().await;
|
let mut state = self.inner.lock().await;
|
||||||
|
|
||||||
if !self.platform_supported() {
|
if !self.platform_supported() {
|
||||||
let problem = DesktopProblem::unsupported_platform(
|
let problem = DesktopProblem::unsupported_platform(desktop_platform_support_message());
|
||||||
"Desktop APIs are only supported on Linux hosts and sandboxes",
|
|
||||||
);
|
|
||||||
self.record_problem_locked(&mut state, &problem);
|
self.record_problem_locked(&mut state, &problem);
|
||||||
state.state = DesktopState::Failed;
|
state.state = DesktopState::Failed;
|
||||||
return Err(problem);
|
return Err(problem);
|
||||||
|
|
@ -182,6 +181,7 @@ impl DesktopRuntime {
|
||||||
height,
|
height,
|
||||||
dpi: Some(dpi),
|
dpi: Some(dpi),
|
||||||
};
|
};
|
||||||
|
let environment = self.base_environment(&display)?;
|
||||||
|
|
||||||
state.state = DesktopState::Starting;
|
state.state = DesktopState::Starting;
|
||||||
state.display_num = display_num;
|
state.display_num = display_num;
|
||||||
|
|
@ -189,13 +189,21 @@ impl DesktopRuntime {
|
||||||
state.resolution = Some(resolution.clone());
|
state.resolution = Some(resolution.clone());
|
||||||
state.started_at = None;
|
state.started_at = None;
|
||||||
state.last_error = None;
|
state.last_error = None;
|
||||||
state.environment = self.base_environment(&display)?;
|
state.environment = environment;
|
||||||
state.install_command = None;
|
state.install_command = None;
|
||||||
|
|
||||||
self.start_dbus_locked(&mut state).await?;
|
if let Err(problem) = self.start_dbus_locked(&mut state).await {
|
||||||
self.start_xvfb_locked(&mut state, &resolution).await?;
|
return Err(self.fail_start_locked(&mut state, problem).await);
|
||||||
self.wait_for_socket(display_num).await?;
|
}
|
||||||
self.start_openbox_locked(&mut state).await?;
|
if let Err(problem) = self.start_xvfb_locked(&mut state, &resolution).await {
|
||||||
|
return Err(self.fail_start_locked(&mut state, problem).await);
|
||||||
|
}
|
||||||
|
if let Err(problem) = self.wait_for_socket(display_num).await {
|
||||||
|
return Err(self.fail_start_locked(&mut state, problem).await);
|
||||||
|
}
|
||||||
|
if let Err(problem) = self.start_openbox_locked(&mut state).await {
|
||||||
|
return Err(self.fail_start_locked(&mut state, problem).await);
|
||||||
|
}
|
||||||
|
|
||||||
let ready = DesktopReadyContext {
|
let ready = DesktopReadyContext {
|
||||||
display,
|
display,
|
||||||
|
|
@ -203,23 +211,15 @@ impl DesktopRuntime {
|
||||||
resolution,
|
resolution,
|
||||||
};
|
};
|
||||||
|
|
||||||
let display_info = self
|
let display_info = match self.query_display_info_locked(&state, &ready).await {
|
||||||
.query_display_info_locked(&state, &ready)
|
Ok(display_info) => display_info,
|
||||||
.await
|
Err(problem) => return Err(self.fail_start_locked(&mut state, problem).await),
|
||||||
.map_err(|problem| {
|
};
|
||||||
self.record_problem_locked(&mut state, &problem);
|
|
||||||
state.state = DesktopState::Failed;
|
|
||||||
problem
|
|
||||||
})?;
|
|
||||||
state.resolution = Some(display_info.resolution.clone());
|
state.resolution = Some(display_info.resolution.clone());
|
||||||
|
|
||||||
self.capture_screenshot_locked(&state, None)
|
if let Err(problem) = self.capture_screenshot_locked(&state, None).await {
|
||||||
.await
|
return Err(self.fail_start_locked(&mut state, problem).await);
|
||||||
.map_err(|problem| {
|
}
|
||||||
self.record_problem_locked(&mut state, &problem);
|
|
||||||
state.state = DesktopState::Failed;
|
|
||||||
problem
|
|
||||||
})?;
|
|
||||||
|
|
||||||
state.state = DesktopState::Active;
|
state.state = DesktopState::Active;
|
||||||
state.started_at = Some(chrono::Utc::now().to_rfc3339());
|
state.started_at = Some(chrono::Utc::now().to_rfc3339());
|
||||||
|
|
@ -403,12 +403,7 @@ impl DesktopRuntime {
|
||||||
|
|
||||||
let mut state = self.inner.lock().await;
|
let mut state = self.inner.lock().await;
|
||||||
let ready = self.ensure_ready_locked(&mut state).await?;
|
let ready = self.ensure_ready_locked(&mut state).await?;
|
||||||
let args = vec![
|
let args = type_text_args(request.text, request.delay_ms.unwrap_or(10));
|
||||||
"type".to_string(),
|
|
||||||
"--delay".to_string(),
|
|
||||||
request.delay_ms.unwrap_or(10).to_string(),
|
|
||||||
request.text,
|
|
||||||
];
|
|
||||||
self.run_input_command_locked(&state, &ready, args).await?;
|
self.run_input_command_locked(&state, &ready, args).await?;
|
||||||
Ok(DesktopActionResponse { ok: true })
|
Ok(DesktopActionResponse { ok: true })
|
||||||
}
|
}
|
||||||
|
|
@ -423,7 +418,7 @@ impl DesktopRuntime {
|
||||||
|
|
||||||
let mut state = self.inner.lock().await;
|
let mut state = self.inner.lock().await;
|
||||||
let ready = self.ensure_ready_locked(&mut state).await?;
|
let ready = self.ensure_ready_locked(&mut state).await?;
|
||||||
let args = vec!["key".to_string(), request.key];
|
let args = press_key_args(request.key);
|
||||||
self.run_input_command_locked(&state, &ready, args).await?;
|
self.run_input_command_locked(&state, &ready, args).await?;
|
||||||
Ok(DesktopActionResponse { ok: true })
|
Ok(DesktopActionResponse { ok: true })
|
||||||
}
|
}
|
||||||
|
|
@ -496,10 +491,8 @@ impl DesktopRuntime {
|
||||||
if !self.platform_supported() {
|
if !self.platform_supported() {
|
||||||
state.state = DesktopState::Failed;
|
state.state = DesktopState::Failed;
|
||||||
state.last_error = Some(
|
state.last_error = Some(
|
||||||
DesktopProblem::unsupported_platform(
|
DesktopProblem::unsupported_platform(desktop_platform_support_message())
|
||||||
"Desktop APIs are only supported on Linux hosts and sandboxes",
|
.to_error_info(),
|
||||||
)
|
|
||||||
.to_error_info(),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -527,6 +520,15 @@ impl DesktopRuntime {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state.state == DesktopState::Failed
|
||||||
|
&& state.display.is_none()
|
||||||
|
&& state.xvfb.is_none()
|
||||||
|
&& state.openbox.is_none()
|
||||||
|
&& state.dbus_pid.is_none()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(display) = state.display.clone() else {
|
let Some(display) = state.display.clone() else {
|
||||||
state.state = DesktopState::Failed;
|
state.state = DesktopState::Failed;
|
||||||
state.last_error = Some(
|
state.last_error = Some(
|
||||||
|
|
@ -737,6 +739,24 @@ impl DesktopRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn fail_start_locked(
|
||||||
|
&self,
|
||||||
|
state: &mut DesktopRuntimeStateData,
|
||||||
|
problem: DesktopProblem,
|
||||||
|
) -> DesktopProblem {
|
||||||
|
self.record_problem_locked(state, &problem);
|
||||||
|
self.write_runtime_log_locked(state, "desktop runtime startup failed; cleaning up");
|
||||||
|
self.stop_openbox_locked(state).await;
|
||||||
|
self.stop_xvfb_locked(state).await;
|
||||||
|
self.stop_dbus_locked(state);
|
||||||
|
state.state = DesktopState::Failed;
|
||||||
|
state.display = None;
|
||||||
|
state.resolution = None;
|
||||||
|
state.started_at = None;
|
||||||
|
state.environment.clear();
|
||||||
|
problem
|
||||||
|
}
|
||||||
|
|
||||||
async fn capture_screenshot_locked(
|
async fn capture_screenshot_locked(
|
||||||
&self,
|
&self,
|
||||||
state: &DesktopRuntimeStateData,
|
state: &DesktopRuntimeStateData,
|
||||||
|
|
@ -1274,6 +1294,20 @@ fn parse_mouse_position(bytes: &[u8]) -> Result<DesktopMousePositionResponse, St
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn type_text_args(text: String, delay_ms: u32) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"type".to_string(),
|
||||||
|
"--delay".to_string(),
|
||||||
|
delay_ms.to_string(),
|
||||||
|
"--".to_string(),
|
||||||
|
text,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn press_key_args(key: String) -> Vec<String> {
|
||||||
|
vec!["key".to_string(), "--".to_string(), key]
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_start_request(width: u32, height: u32, dpi: u32) -> Result<(), DesktopProblem> {
|
fn validate_start_request(width: u32, height: u32, dpi: u32) -> Result<(), DesktopProblem> {
|
||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
return Err(DesktopProblem::invalid_action(
|
return Err(DesktopProblem::invalid_action(
|
||||||
|
|
@ -1356,4 +1390,16 @@ mod tests {
|
||||||
let error = validate_png(b"not png").expect_err("validation should fail");
|
let error = validate_png(b"not png").expect_err("validation should fail");
|
||||||
assert!(error.contains("PNG"));
|
assert!(error.contains("PNG"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_text_args_insert_double_dash_before_user_text() {
|
||||||
|
let args = type_text_args("--help".to_string(), 5);
|
||||||
|
assert_eq!(args, vec!["type", "--delay", "5", "--", "--help"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn press_key_args_insert_double_dash_before_user_key() {
|
||||||
|
let args = press_key_args("--help".to_string());
|
||||||
|
assert_eq!(args, vec!["key", "--", "--help"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ use std::thread;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use sandbox_agent::router::AuthConfig;
|
use sandbox_agent::router::AuthConfig;
|
||||||
|
use serial_test::serial;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
const CONTAINER_PORT: u16 = 3000;
|
const CONTAINER_PORT: u16 = 3000;
|
||||||
const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||||
|
const DEFAULT_IMAGE_TAG: &str = "sandbox-agent-test:dev";
|
||||||
const STANDARD_PATHS: &[&str] = &[
|
const STANDARD_PATHS: &[&str] = &[
|
||||||
"/usr/local/sbin",
|
"/usr/local/sbin",
|
||||||
"/usr/local/bin",
|
"/usr/local/bin",
|
||||||
|
|
@ -183,24 +185,26 @@ fn ensure_test_image() -> String {
|
||||||
IMAGE_TAG
|
IMAGE_TAG
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let repo_root = repo_root();
|
let repo_root = repo_root();
|
||||||
let script = repo_root
|
let image_tag = std::env::var("SANDBOX_AGENT_TEST_IMAGE")
|
||||||
.join("scripts")
|
.unwrap_or_else(|_| DEFAULT_IMAGE_TAG.to_string());
|
||||||
.join("test-rig")
|
let output = Command::new(docker_bin())
|
||||||
.join("ensure-image.sh");
|
.args(["build", "--tag", &image_tag, "--file"])
|
||||||
let output = Command::new("/bin/bash")
|
.arg(
|
||||||
.arg(&script)
|
repo_root
|
||||||
|
.join("docker")
|
||||||
|
.join("test-agent")
|
||||||
|
.join("Dockerfile"),
|
||||||
|
)
|
||||||
|
.arg(&repo_root)
|
||||||
.output()
|
.output()
|
||||||
.expect("run ensure-image.sh");
|
.expect("build sandbox-agent test image");
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
panic!(
|
panic!(
|
||||||
"failed to build sandbox-agent test image: {}",
|
"failed to build sandbox-agent test image: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
String::from_utf8(output.stdout)
|
image_tag
|
||||||
.expect("image tag utf8")
|
|
||||||
.trim()
|
|
||||||
.to_string()
|
|
||||||
})
|
})
|
||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -235,12 +239,6 @@ fn build_env(
|
||||||
"LOCALAPPDATA".to_string(),
|
"LOCALAPPDATA".to_string(),
|
||||||
layout.local_appdata.to_string_lossy().to_string(),
|
layout.local_appdata.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
if let Some(value) = std::env::var_os("XDG_STATE_HOME") {
|
|
||||||
env.insert(
|
|
||||||
"XDG_STATE_HOME".to_string(),
|
|
||||||
PathBuf::from(value).to_string_lossy().to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, value) in std::env::vars() {
|
for (key, value) in std::env::vars() {
|
||||||
if key == "PATH" {
|
if key == "PATH" {
|
||||||
|
|
@ -549,3 +547,47 @@ fn docker_bin() -> &'static Path {
|
||||||
})
|
})
|
||||||
.as_path()
|
.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct EnvVarGuard {
|
||||||
|
key: &'static str,
|
||||||
|
old: Option<std::ffi::OsString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvVarGuard {
|
||||||
|
fn set(key: &'static str, value: &Path) -> Self {
|
||||||
|
let old = std::env::var_os(key);
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
Self { key, old }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for EnvVarGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
match self.old.as_ref() {
|
||||||
|
Some(value) => std::env::set_var(self.key, value),
|
||||||
|
None => std::env::remove_var(self.key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn build_env_keeps_test_local_xdg_state_home() {
|
||||||
|
let root = tempfile::tempdir().expect("create docker support tempdir");
|
||||||
|
let host_state = tempfile::tempdir().expect("create host xdg state tempdir");
|
||||||
|
let _guard = EnvVarGuard::set("XDG_STATE_HOME", host_state.path());
|
||||||
|
|
||||||
|
let layout = TestLayout::new(root.path());
|
||||||
|
layout.create();
|
||||||
|
|
||||||
|
let env = build_env(&layout, &AuthConfig::disabled(), &TestAppOptions::default());
|
||||||
|
assert_eq!(
|
||||||
|
env.get("XDG_STATE_HOME"),
|
||||||
|
Some(&layout.xdg_state_home.to_string_lossy().to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue