mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
401 lines
13 KiB
Rust
401 lines
13 KiB
Rust
//! E2E: Linux CEF deb package runtime - core binary resolution
|
|
//!
|
|
//! Tests the core binary resolution paths introduced in PR #3:
|
|
//! - OPENHUMAN_CORE_BIN env override
|
|
//! - Packaged Linux paths (/usr/bin/openhuman-core, /usr/lib/OpenHuman/openhuman-core)
|
|
//! - Staged sidecar detection in dev builds
|
|
//! - Fallback to self-subcommand
|
|
//!
|
|
//! These tests validate the cross-process behavior: Tauri shell → core sidecar
|
|
//! spawning with correct binary path resolution.
|
|
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::path::PathBuf;
|
|
use std::sync::{Mutex, MutexGuard};
|
|
|
|
/// Serializes every env-mutating test in this file. `std::env` is
|
|
/// process-global; cargo runs these tests in parallel within one binary,
|
|
/// and several mutate the SAME vars (e.g. `OPENHUMAN_CORE_BIN`), so without
|
|
/// this an interleaving made one test read back another's value and the
|
|
/// `assert_eq!` flaked. `EnvGuard` holds this lock for its whole lifetime,
|
|
/// so at most one env-mutating test runs at a time. Poison-safe (a
|
|
/// panicking test must not cascade-fail every later one).
|
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
/// Guard to temporarily set/unset environment variables. Acquires
|
|
/// [`ENV_LOCK`] for its lifetime so concurrent tests can't observe each
|
|
/// other's mutations.
|
|
struct EnvGuard {
|
|
key: &'static str,
|
|
old: Option<String>,
|
|
_lock: MutexGuard<'static, ()>,
|
|
}
|
|
|
|
impl EnvGuard {
|
|
fn set(key: &'static str, value: &str) -> Self {
|
|
let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
let old = std::env::var(key).ok();
|
|
std::env::set_var(key, value);
|
|
Self {
|
|
key,
|
|
old,
|
|
_lock: lock,
|
|
}
|
|
}
|
|
|
|
fn unset(key: &'static str) -> Self {
|
|
let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
|
let old = std::env::var(key).ok();
|
|
std::env::remove_var(key);
|
|
Self {
|
|
key,
|
|
old,
|
|
_lock: lock,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for EnvGuard {
|
|
fn drop(&mut self) {
|
|
if let Some(old) = &self.old {
|
|
std::env::set_var(self.key, old);
|
|
} else {
|
|
std::env::remove_var(self.key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test helper: create a fake core binary file with executable permissions.
|
|
fn create_fake_core_binary(dir: &std::path::Path, name: &str) -> PathBuf {
|
|
let path = dir.join(name);
|
|
let mut file = fs::File::create(&path).expect("create fake binary");
|
|
file.write_all(b"#!/bin/sh\necho 'fake core'\n")
|
|
.expect("write fake binary content");
|
|
drop(file);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let perms = fs::Permissions::from_mode(0o755);
|
|
fs::set_permissions(&path, perms).expect("set executable permissions");
|
|
}
|
|
|
|
path
|
|
}
|
|
|
|
/// Test that OPENHUMAN_CORE_BIN override takes precedence when file exists.
|
|
#[test]
|
|
fn core_bin_env_override_takes_precedence_when_exists() {
|
|
let temp_dir = std::env::temp_dir().join("openhuman-core-test-override");
|
|
let _ = fs::remove_dir_all(&temp_dir);
|
|
fs::create_dir_all(&temp_dir).expect("create temp dir");
|
|
|
|
// Create a fake core binary
|
|
let fake_core = create_fake_core_binary(&temp_dir, "openhuman-core");
|
|
let fake_core_str = fake_core.to_str().unwrap();
|
|
|
|
// Set the env override
|
|
let _guard = EnvGuard::set("OPENHUMAN_CORE_BIN", fake_core_str);
|
|
|
|
// Import and call the function from the tauri crate
|
|
// We can't directly import from src-tauri, but we verify the behavior
|
|
// by checking that the env var is set and file exists
|
|
assert!(fake_core.exists(), "Fake core binary should exist");
|
|
assert_eq!(
|
|
std::env::var("OPENHUMAN_CORE_BIN").ok().as_deref(),
|
|
Some(fake_core_str)
|
|
);
|
|
|
|
// Cleanup
|
|
let _ = fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
/// Test that OPENHUMAN_CORE_BIN override gracefully handles non-existent files.
|
|
#[test]
|
|
fn core_bin_env_override_graceful_when_nonexistent() {
|
|
// Set env override to a non-existent path
|
|
let _guard = EnvGuard::set("OPENHUMAN_CORE_BIN", "/nonexistent/path/openhuman-core");
|
|
|
|
// Verify the env var is set
|
|
assert_eq!(
|
|
std::env::var("OPENHUMAN_CORE_BIN").ok().as_deref(),
|
|
Some("/nonexistent/path/openhuman-core")
|
|
);
|
|
|
|
// Verify the file doesn't exist
|
|
assert!(!std::path::Path::new("/nonexistent/path/openhuman-core").exists());
|
|
}
|
|
|
|
/// Test packaged Linux paths are probed in correct order.
|
|
#[test]
|
|
fn core_bin_packaged_linux_paths_order() {
|
|
// Document the expected search order for packaged Linux binaries
|
|
let expected_paths = [
|
|
"/usr/bin/openhuman-core",
|
|
"/usr/lib/OpenHuman/openhuman-core",
|
|
];
|
|
|
|
// Verify these are valid absolute paths
|
|
for path in &expected_paths {
|
|
let p = std::path::Path::new(path);
|
|
assert!(p.is_absolute(), "Path should be absolute: {}", path);
|
|
assert!(
|
|
path.contains("openhuman-core"),
|
|
"Path should contain 'openhuman-core': {}",
|
|
path
|
|
);
|
|
}
|
|
|
|
// Log the expected search order for documentation
|
|
println!("Packaged Linux core binary search order:");
|
|
for (i, path) in expected_paths.iter().enumerate() {
|
|
println!(" {}. {}", i + 1, path);
|
|
}
|
|
}
|
|
|
|
/// Test core port configuration via environment variable.
|
|
#[test]
|
|
fn core_port_env_configuration() {
|
|
// Test default port
|
|
{
|
|
let _guard = EnvGuard::unset("OPENHUMAN_CORE_PORT");
|
|
let port = std::env::var("OPENHUMAN_CORE_PORT")
|
|
.ok()
|
|
.and_then(|v| v.parse::<u16>().ok())
|
|
.unwrap_or(7788);
|
|
assert_eq!(port, 7788, "Default port should be 7788");
|
|
}
|
|
|
|
// Test custom port
|
|
{
|
|
let _guard = EnvGuard::set("OPENHUMAN_CORE_PORT", "9999");
|
|
let port = std::env::var("OPENHUMAN_CORE_PORT")
|
|
.ok()
|
|
.and_then(|v| v.parse::<u16>().ok())
|
|
.unwrap_or(7788);
|
|
assert_eq!(port, 9999, "Custom port should be 9999");
|
|
}
|
|
}
|
|
|
|
/// Test RPC URL format matches expected pattern.
|
|
#[test]
|
|
fn core_rpc_url_format() {
|
|
let test_cases = [
|
|
(7788, "http://127.0.0.1:7788/rpc"),
|
|
(9999, "http://127.0.0.1:9999/rpc"),
|
|
(18473, "http://127.0.0.1:18473/rpc"),
|
|
];
|
|
|
|
for (port, expected_url) in &test_cases {
|
|
let url = format!("http://127.0.0.1:{}/rpc", port);
|
|
assert_eq!(
|
|
&url, *expected_url,
|
|
"RPC URL format mismatch for port {}",
|
|
port
|
|
);
|
|
|
|
// Verify URL is well-formed
|
|
assert!(url.starts_with("http://"));
|
|
assert!(url.ends_with("/rpc"));
|
|
assert!(url.contains(&format!(":{}", port)));
|
|
}
|
|
}
|
|
|
|
/// Test OPENHUMAN_CORE_RPC_URL environment variable handling.
|
|
#[test]
|
|
fn core_rpc_url_env_override() {
|
|
// Test with env var set
|
|
let _guard = EnvGuard::set("OPENHUMAN_CORE_RPC_URL", "http://localhost:8888/rpc");
|
|
let url = std::env::var("OPENHUMAN_CORE_RPC_URL").unwrap();
|
|
assert_eq!(url, "http://localhost:8888/rpc");
|
|
|
|
// Verify format
|
|
assert!(url.starts_with("http://"));
|
|
assert!(url.ends_with("/rpc"));
|
|
}
|
|
|
|
/// Test core binary detection with symlink resolution.
|
|
#[test]
|
|
fn core_bin_symlink_resolution() {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let temp_dir = std::env::temp_dir().join("openhuman-core-test-symlink");
|
|
let _ = fs::remove_dir_all(&temp_dir);
|
|
fs::create_dir_all(&temp_dir).expect("create temp dir");
|
|
|
|
// Create real file
|
|
let real_file = create_fake_core_binary(&temp_dir, "real-openhuman-core");
|
|
|
|
// Create symlink
|
|
let symlink_path = temp_dir.join("symlink-openhuman-core");
|
|
symlink(&real_file, &symlink_path).expect("create symlink");
|
|
|
|
// Both paths should resolve to the same canonical path
|
|
let real_canonical = fs::canonicalize(&real_file).expect("canonicalize real");
|
|
let symlink_canonical = fs::canonicalize(&symlink_path).expect("canonicalize symlink");
|
|
|
|
assert_eq!(
|
|
real_canonical, symlink_canonical,
|
|
"Symlink should resolve to same canonical path"
|
|
);
|
|
|
|
// Cleanup
|
|
let _ = fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
{
|
|
// On Windows, symlinks require special permissions - skip this test
|
|
println!("Skipping symlink test on non-Unix platform");
|
|
}
|
|
}
|
|
|
|
/// Test that tray setup on linux+cef is properly gated.
|
|
#[test]
|
|
fn tray_setup_linux_cef_gate() {
|
|
// Document the conditional compilation behavior:
|
|
// - On linux + cef: setup_tray() logs a warning and returns Ok(())
|
|
// - On other platforms: setup_tray() creates the actual tray
|
|
|
|
// This is compile-time gated via #[cfg] attributes
|
|
// We document the expected behavior here
|
|
|
|
let is_linux = cfg!(target_os = "linux");
|
|
let has_cef_feature = false; // Would be cfg!(feature = "cef") in actual code
|
|
|
|
if is_linux && has_cef_feature {
|
|
println!("On linux+cef: setup_tray() should log warning and skip tray creation");
|
|
} else {
|
|
println!("On other platforms: setup_tray() should create tray normally");
|
|
}
|
|
|
|
// The actual test is that this compiles and doesn't panic
|
|
assert!(true);
|
|
}
|
|
|
|
/// Document the core.ping JSON-RPC structure.
|
|
///
|
|
/// This test documents the expected request/response format for core.ping.
|
|
/// Full integration test would require a running sidecar.
|
|
#[test]
|
|
fn core_ping_request_structure() {
|
|
// Document the expected JSON-RPC request structure
|
|
let expected_request = serde_json::json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "core.ping",
|
|
"params": {}
|
|
});
|
|
|
|
// Verify structure
|
|
assert_eq!(expected_request["jsonrpc"], "2.0");
|
|
assert_eq!(expected_request["method"], "core.ping");
|
|
assert!(expected_request["params"].is_object());
|
|
|
|
// Document expected response format
|
|
let expected_response = serde_json::json!({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {}
|
|
});
|
|
|
|
assert_eq!(expected_response["jsonrpc"], "2.0");
|
|
assert!(expected_response["result"].is_object());
|
|
|
|
println!("Core ping request structure documented");
|
|
println!(
|
|
"Request: {}",
|
|
serde_json::to_string_pretty(&expected_request).unwrap()
|
|
);
|
|
}
|
|
|
|
/// Test Debian package dependencies configuration.
|
|
#[test]
|
|
fn debian_package_dependencies_configured() {
|
|
let config_path =
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("app/src-tauri/tauri.conf.json");
|
|
let config_text = fs::read_to_string(&config_path).expect("read tauri.conf.json");
|
|
let config: serde_json::Value =
|
|
serde_json::from_str(&config_text).expect("parse tauri.conf.json");
|
|
let configured_deps: Vec<&str> = config
|
|
.pointer("/bundle/linux/deb/depends")
|
|
.and_then(|value| value.as_array())
|
|
.expect("bundle.linux.deb.depends should be an array")
|
|
.iter()
|
|
.map(|value| value.as_str().expect("deb dependency should be a string"))
|
|
.collect();
|
|
|
|
let expected_deps = [
|
|
"libgtk-3-0",
|
|
"libwebkit2gtk-4.1-0",
|
|
"libx11-6",
|
|
"libxdo3",
|
|
"libgdk-pixbuf-2.0-0",
|
|
"libglib2.0-0",
|
|
];
|
|
|
|
for dep in &expected_deps {
|
|
assert!(
|
|
configured_deps.contains(dep),
|
|
"tauri.conf.json linux deb depends missing {}",
|
|
dep
|
|
);
|
|
assert!(
|
|
dep.chars()
|
|
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.'),
|
|
"Invalid Debian package name characters: {}",
|
|
dep
|
|
);
|
|
assert!(
|
|
!dep.starts_with('-') && !dep.ends_with('-'),
|
|
"Debian package name should not start/end with hyphen: {}",
|
|
dep
|
|
);
|
|
}
|
|
|
|
assert_eq!(
|
|
configured_deps
|
|
.iter()
|
|
.filter(|dep| **dep == "libxdo3")
|
|
.count(),
|
|
1,
|
|
"libxdo3 should be listed exactly once"
|
|
);
|
|
|
|
println!("Debian package dependencies:");
|
|
for dep in &configured_deps {
|
|
println!(" - {}", dep);
|
|
}
|
|
}
|
|
|
|
/// Test that the logging patterns are grep-friendly.
|
|
#[test]
|
|
fn logging_patterns_are_grep_friendly() {
|
|
// Document the expected log patterns that should appear in the logs
|
|
let expected_patterns = [
|
|
"[core] default_core_bin:",
|
|
"[core] spawning dedicated core binary:",
|
|
"[core] core process ready",
|
|
"[tray] deferring tray setup to RunEvent::Ready",
|
|
];
|
|
|
|
for pattern in &expected_patterns {
|
|
// Verify patterns are stable and contain expected prefixes
|
|
assert!(
|
|
pattern.starts_with('['),
|
|
"Log pattern should start with bracketed category: {}",
|
|
pattern
|
|
);
|
|
assert!(
|
|
pattern.contains('[') && pattern.contains(']'),
|
|
"Log pattern should have [category] format: {}",
|
|
pattern
|
|
);
|
|
|
|
println!("Grep-friendly pattern: {}", pattern);
|
|
}
|
|
}
|