feat: improve testing (#6395)
* add more test rust test * fix servicehub test * fix tauri failing on windows
This commit is contained in:
parent
dbaf563e88
commit
5cd81bc6e8
1
Makefile
1
Makefile
@ -65,6 +65,7 @@ test: lint
|
||||
cargo test --manifest-path src-tauri/Cargo.toml --no-default-features --features test-tauri -- --test-threads=1
|
||||
cargo test --manifest-path src-tauri/plugins/tauri-plugin-hardware/Cargo.toml
|
||||
cargo test --manifest-path src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml
|
||||
cargo test --manifest-path src-tauri/utils/Cargo.toml
|
||||
|
||||
# Builds and publishes the app
|
||||
build-and-publish: install-and-build
|
||||
|
||||
54
mise.toml
54
mise.toml
@ -138,11 +138,56 @@ description = "Run linting (matches Makefile)"
|
||||
depends = ["build-extensions"]
|
||||
run = "yarn lint"
|
||||
|
||||
[tasks.test]
|
||||
description = "Run test suite (matches Makefile)"
|
||||
depends = ["lint"]
|
||||
# ============================================================================
|
||||
# RUST TEST COMPONENTS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.test-rust-main]
|
||||
description = "Test main src-tauri package"
|
||||
run = "cargo test --manifest-path src-tauri/Cargo.toml --no-default-features --features test-tauri -- --test-threads=1"
|
||||
|
||||
[tasks.test-rust-hardware]
|
||||
description = "Test hardware plugin"
|
||||
run = "cargo test --manifest-path src-tauri/plugins/tauri-plugin-hardware/Cargo.toml"
|
||||
|
||||
[tasks.test-rust-llamacpp]
|
||||
description = "Test llamacpp plugin"
|
||||
run = "cargo test --manifest-path src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml"
|
||||
|
||||
[tasks.test-rust-utils]
|
||||
description = "Test utils package"
|
||||
run = "cargo test --manifest-path src-tauri/utils/Cargo.toml"
|
||||
|
||||
[tasks.test-rust]
|
||||
description = "Run all Rust tests"
|
||||
depends = ["test-rust-main", "test-rust-hardware", "test-rust-llamacpp", "test-rust-utils"]
|
||||
|
||||
# ============================================================================
|
||||
# JS TEST COMPONENTS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.test-js-setup]
|
||||
description = "Setup for JS tests"
|
||||
run = [
|
||||
"yarn download:bin",
|
||||
"yarn download:lib",
|
||||
"yarn copy:assets:tauri",
|
||||
"yarn build:icon"
|
||||
]
|
||||
|
||||
[tasks.test-js]
|
||||
description = "Run JS tests"
|
||||
depends = ["test-js-setup"]
|
||||
run = "yarn test"
|
||||
|
||||
# ============================================================================
|
||||
# COMBINED TEST TASKS
|
||||
# ============================================================================
|
||||
|
||||
[tasks.test]
|
||||
description = "Run complete test suite (matches Makefile)"
|
||||
depends = ["lint", "test-js", "test-rust"]
|
||||
|
||||
# ============================================================================
|
||||
# PARALLEL-FRIENDLY QUALITY ASSURANCE TASKS
|
||||
# ============================================================================
|
||||
@ -155,8 +200,7 @@ hide = true
|
||||
|
||||
[tasks.test-only]
|
||||
description = "Run tests only (parallel-friendly)"
|
||||
depends = ["build-extensions"]
|
||||
run = "yarn test"
|
||||
depends = ["build-extensions", "test-js", "test-rust"]
|
||||
hide = true
|
||||
|
||||
[tasks.qa-parallel]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "jan-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"core",
|
||||
|
||||
@ -33,5 +33,8 @@ windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] }
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "=0.30.1", features = ["signal", "process"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.3.1", features = ["build"] }
|
||||
|
||||
@ -228,3 +228,186 @@ fn parse_memory_value(mem_str: &str) -> ServerResult<i32> {
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_memory_pattern_valid() {
|
||||
assert!(is_memory_pattern("8128 MiB, 8128 MiB free"));
|
||||
assert!(is_memory_pattern("1024 MiB, 512 MiB free"));
|
||||
assert!(is_memory_pattern("16384 MiB, 12000 MiB free"));
|
||||
assert!(is_memory_pattern("0 MiB, 0 MiB free"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_memory_pattern_invalid() {
|
||||
assert!(!is_memory_pattern("8128 MB, 8128 MB free")); // Wrong unit
|
||||
assert!(!is_memory_pattern("8128 MiB 8128 MiB free")); // Missing comma
|
||||
assert!(!is_memory_pattern("8128 MiB, 8128 MiB used")); // Wrong second part
|
||||
assert!(!is_memory_pattern("not_a_number MiB, 8128 MiB free")); // Invalid number
|
||||
assert!(!is_memory_pattern("8128 MiB")); // Missing second part
|
||||
assert!(!is_memory_pattern("")); // Empty string
|
||||
assert!(!is_memory_pattern("8128 MiB, free")); // Missing number in second part
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern() {
|
||||
let text = "Intel(R) Arc(tm) A750 Graphics (DG2) (8128 MiB, 4096 MiB free)";
|
||||
let result = find_memory_pattern(text);
|
||||
assert!(result.is_some());
|
||||
let (start_idx, content) = result.unwrap();
|
||||
assert!(start_idx > 0);
|
||||
assert_eq!(content, "8128 MiB, 4096 MiB free");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern_multiple_parentheses() {
|
||||
let text = "Device (test) with (1024 MiB, 512 MiB free) and (2048 MiB, 1024 MiB free)";
|
||||
let result = find_memory_pattern(text);
|
||||
assert!(result.is_some());
|
||||
let (_, content) = result.unwrap();
|
||||
// Should return the LAST valid memory pattern
|
||||
assert_eq!(content, "2048 MiB, 1024 MiB free");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern_no_match() {
|
||||
let text = "No memory info here";
|
||||
assert!(find_memory_pattern(text).is_none());
|
||||
|
||||
let text_with_invalid = "Some text (invalid memory info) here";
|
||||
assert!(find_memory_pattern(text_with_invalid).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_memory_value() {
|
||||
assert_eq!(parse_memory_value("8128 MiB").unwrap(), 8128);
|
||||
assert_eq!(parse_memory_value("7721 MiB free").unwrap(), 7721);
|
||||
assert_eq!(parse_memory_value("0 MiB").unwrap(), 0);
|
||||
assert_eq!(parse_memory_value("24576 MiB").unwrap(), 24576);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_memory_value_invalid() {
|
||||
assert!(parse_memory_value("").is_err());
|
||||
assert!(parse_memory_value("not_a_number MiB").is_err());
|
||||
assert!(parse_memory_value(" ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_line_vulkan() {
|
||||
let line = "Vulkan0: Intel(R) Arc(tm) A750 Graphics (DG2) (8128 MiB, 8128 MiB free)";
|
||||
let result = parse_device_line(line).unwrap();
|
||||
assert!(result.is_some());
|
||||
let device = result.unwrap();
|
||||
assert_eq!(device.id, "Vulkan0");
|
||||
assert_eq!(device.name, "Intel(R) Arc(tm) A750 Graphics (DG2)");
|
||||
assert_eq!(device.mem, 8128);
|
||||
assert_eq!(device.free, 8128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_line_cuda() {
|
||||
let line = "CUDA0: NVIDIA GeForce RTX 4090 (24576 MiB, 24000 MiB free)";
|
||||
let result = parse_device_line(line).unwrap();
|
||||
assert!(result.is_some());
|
||||
let device = result.unwrap();
|
||||
assert_eq!(device.id, "CUDA0");
|
||||
assert_eq!(device.name, "NVIDIA GeForce RTX 4090");
|
||||
assert_eq!(device.mem, 24576);
|
||||
assert_eq!(device.free, 24000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_line_sycl() {
|
||||
let line = "SYCL0: Intel(R) Arc(TM) A750 Graphics (8000 MiB, 7721 MiB free)";
|
||||
let result = parse_device_line(line).unwrap();
|
||||
assert!(result.is_some());
|
||||
let device = result.unwrap();
|
||||
assert_eq!(device.id, "SYCL0");
|
||||
assert_eq!(device.name, "Intel(R) Arc(TM) A750 Graphics");
|
||||
assert_eq!(device.mem, 8000);
|
||||
assert_eq!(device.free, 7721);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_line_malformed() {
|
||||
// Missing colon
|
||||
let result = parse_device_line("Vulkan0 Intel Graphics (8128 MiB, 8128 MiB free)").unwrap();
|
||||
assert!(result.is_none());
|
||||
|
||||
// Missing memory info
|
||||
let result = parse_device_line("Vulkan0: Intel Graphics").unwrap();
|
||||
assert!(result.is_none());
|
||||
|
||||
// Invalid memory format
|
||||
let result = parse_device_line("Vulkan0: Intel Graphics (invalid memory)").unwrap();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_output_valid() {
|
||||
let output = r#"
|
||||
Some header text
|
||||
Available devices:
|
||||
Vulkan0: Intel(R) Arc(tm) A750 Graphics (DG2) (8128 MiB, 8128 MiB free)
|
||||
CUDA0: NVIDIA GeForce RTX 4090 (24576 MiB, 24000 MiB free)
|
||||
|
||||
SYCL0: Intel(R) Arc(TM) A750 Graphics (8000 MiB, 7721 MiB free)
|
||||
Some footer text
|
||||
"#;
|
||||
|
||||
let result = parse_device_output(output).unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
|
||||
assert_eq!(result[0].id, "Vulkan0");
|
||||
assert_eq!(result[0].name, "Intel(R) Arc(tm) A750 Graphics (DG2)");
|
||||
assert_eq!(result[0].mem, 8128);
|
||||
|
||||
assert_eq!(result[1].id, "CUDA0");
|
||||
assert_eq!(result[1].name, "NVIDIA GeForce RTX 4090");
|
||||
assert_eq!(result[1].mem, 24576);
|
||||
|
||||
assert_eq!(result[2].id, "SYCL0");
|
||||
assert_eq!(result[2].name, "Intel(R) Arc(TM) A750 Graphics");
|
||||
assert_eq!(result[2].mem, 8000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_output_no_devices_section() {
|
||||
let output = "Some output without Available devices section";
|
||||
let result = parse_device_output(output);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_output_empty_devices() {
|
||||
let output = r#"
|
||||
Some header text
|
||||
Available devices:
|
||||
|
||||
Some footer text
|
||||
"#;
|
||||
let result = parse_device_output(output).unwrap();
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_device_output_mixed_valid_invalid() {
|
||||
let output = r#"
|
||||
Available devices:
|
||||
Vulkan0: Intel(R) Arc(tm) A750 Graphics (DG2) (8128 MiB, 8128 MiB free)
|
||||
InvalidLine: No memory info
|
||||
CUDA0: NVIDIA GeForce RTX 4090 (24576 MiB, 24000 MiB free)
|
||||
AnotherInvalid
|
||||
"#;
|
||||
|
||||
let result = parse_device_output(output).unwrap();
|
||||
assert_eq!(result.len(), 2); // Only valid lines should be parsed
|
||||
|
||||
assert_eq!(result[0].id, "Vulkan0");
|
||||
assert_eq!(result[1].id, "CUDA0");
|
||||
}
|
||||
}
|
||||
@ -145,3 +145,170 @@ pub fn validate_mmproj_path(args: &mut Vec<String>) -> ServerResult<Option<PathB
|
||||
|
||||
Ok(Some(mmproj_path_pb))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_validate_binary_path_existing() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let path = temp_file.path().to_str().unwrap();
|
||||
|
||||
let result = validate_binary_path(path);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), PathBuf::from(path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_binary_path_nonexistent() {
|
||||
let nonexistent_path = "/tmp/definitely_does_not_exist_123456789";
|
||||
let result = validate_binary_path(nonexistent_path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_model_path_valid() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let path = temp_file.path().to_str().unwrap();
|
||||
|
||||
let mut args = vec!["-m".to_string(), path.to_string(), "--verbose".to_string()];
|
||||
let result = validate_model_path(&mut args);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), PathBuf::from(path));
|
||||
// Args should be updated with the path
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, the path might be converted to short path format
|
||||
// Just verify that the path in args[1] points to the same file
|
||||
assert!(PathBuf::from(&args[1]).exists());
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
assert_eq!(args[1], temp_file.path().display().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_model_path_missing_flag() {
|
||||
let mut args = vec!["--verbose".to_string(), "value".to_string()];
|
||||
let result = validate_model_path(&mut args);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_model_path_missing_value() {
|
||||
let mut args = vec!["-m".to_string()];
|
||||
let result = validate_model_path(&mut args);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_model_path_nonexistent_file() {
|
||||
let nonexistent_path = "/tmp/nonexistent_model_123456789.gguf";
|
||||
let mut args = vec!["-m".to_string(), nonexistent_path.to_string()];
|
||||
let result = validate_model_path(&mut args);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_mmproj_path_valid() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let path = temp_file.path().to_str().unwrap();
|
||||
|
||||
let mut args = vec!["--mmproj".to_string(), path.to_string(), "--verbose".to_string()];
|
||||
let result = validate_mmproj_path(&mut args);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_some());
|
||||
// Args should be updated with the path
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, the path might be converted to short path format
|
||||
// Just verify that the path in args[1] points to the same file
|
||||
assert!(PathBuf::from(&args[1]).exists());
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
assert_eq!(args[1], temp_file.path().display().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_mmproj_path_missing() {
|
||||
let mut args = vec!["--verbose".to_string(), "value".to_string()];
|
||||
let result = validate_mmproj_path(&mut args);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none()); // mmproj is optional
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_mmproj_path_missing_value() {
|
||||
let mut args = vec!["--mmproj".to_string()];
|
||||
let result = validate_mmproj_path(&mut args);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_mmproj_path_nonexistent_file() {
|
||||
let nonexistent_path = "/tmp/nonexistent_mmproj_123456789.gguf";
|
||||
let mut args = vec!["--mmproj".to_string(), nonexistent_path.to_string()];
|
||||
let result = validate_mmproj_path(&mut args);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn test_get_short_path() {
|
||||
// Test with a real path that should exist on Windows
|
||||
use std::env;
|
||||
if let Ok(temp_dir) = env::var("TEMP") {
|
||||
let result = get_short_path(&temp_dir);
|
||||
// Should return some short path or None (both are valid)
|
||||
// We can't assert the exact value as it depends on the system
|
||||
println!("Short path result: {:?}", result);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_model_path_multiple_m_flags() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let path = temp_file.path().to_str().unwrap();
|
||||
|
||||
// Multiple -m flags - should use the first one
|
||||
let mut args = vec![
|
||||
"-m".to_string(),
|
||||
path.to_string(),
|
||||
"--verbose".to_string(),
|
||||
"-m".to_string(),
|
||||
"another_path".to_string()
|
||||
];
|
||||
let result = validate_model_path(&mut args);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), PathBuf::from(path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_mmproj_path_multiple_flags() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let path = temp_file.path().to_str().unwrap();
|
||||
|
||||
// Multiple --mmproj flags - should use the first one
|
||||
let mut args = vec![
|
||||
"--mmproj".to_string(),
|
||||
path.to_string(),
|
||||
"--verbose".to_string(),
|
||||
"--mmproj".to_string(),
|
||||
"another_path".to_string()
|
||||
];
|
||||
let result = validate_mmproj_path(&mut args);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let result_path = result.unwrap();
|
||||
assert!(result_path.is_some());
|
||||
assert_eq!(result_path.unwrap(), PathBuf::from(path));
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,13 @@ reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sha2 = "0.10"
|
||||
tokio = { version = "1", features = ["process"] }
|
||||
tokio = { version = "1", features = ["process", "fs", "macros", "rt"] }
|
||||
tokio-util = "0.7.14"
|
||||
url = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
logging = ["log"]
|
||||
|
||||
@ -84,3 +84,108 @@ pub async fn compute_file_sha256_with_cancellation(
|
||||
log::debug!("Hash computation completed for {} bytes", total_read);
|
||||
Ok(hash_hex)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[test]
|
||||
fn test_generate_app_token() {
|
||||
let token1 = generate_app_token();
|
||||
let token2 = generate_app_token();
|
||||
|
||||
// Should be 32 characters long
|
||||
assert_eq!(token1.len(), 32);
|
||||
assert_eq!(token2.len(), 32);
|
||||
|
||||
// Should be different each time
|
||||
assert_ne!(token1, token2);
|
||||
|
||||
// Should only contain alphanumeric characters
|
||||
assert!(token1.chars().all(|c| c.is_alphanumeric()));
|
||||
assert!(token2.chars().all(|c| c.is_alphanumeric()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_api_key() {
|
||||
let model_id = "test-model".to_string();
|
||||
let api_secret = "test-secret".to_string();
|
||||
|
||||
let key1 = generate_api_key(model_id.clone(), api_secret.clone()).unwrap();
|
||||
let key2 = generate_api_key(model_id.clone(), api_secret.clone()).unwrap();
|
||||
|
||||
// Should generate same key for same inputs
|
||||
assert_eq!(key1, key2);
|
||||
|
||||
// Should be base64 encoded (and thus contain base64 characters)
|
||||
assert!(key1.chars().all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '='));
|
||||
|
||||
// Different model_id should produce different key
|
||||
let different_key = generate_api_key("different-model".to_string(), api_secret).unwrap();
|
||||
assert_ne!(key1, different_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_api_key_empty_inputs() {
|
||||
let result = generate_api_key("".to_string(), "secret".to_string());
|
||||
assert!(result.is_ok()); // Should still work with empty model_id
|
||||
|
||||
let result = generate_api_key("model".to_string(), "".to_string());
|
||||
assert!(result.is_ok()); // Should still work with empty secret
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compute_file_sha256_with_cancellation() {
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
// Create a temporary file with known content
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let test_content = b"Hello, World!";
|
||||
temp_file.write_all(test_content).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let token = CancellationToken::new();
|
||||
|
||||
// Compute hash of the file
|
||||
let hash = compute_file_sha256_with_cancellation(temp_file.path(), &token).await.unwrap();
|
||||
|
||||
// Verify it's a valid hex string
|
||||
assert_eq!(hash.len(), 64); // SHA256 is 256 bits = 64 hex chars
|
||||
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
|
||||
// Verify it matches expected SHA256 of "Hello, World!"
|
||||
let expected = "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f";
|
||||
assert_eq!(hash, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compute_file_sha256_cancellation() {
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
// Create a temporary file
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(b"test content").unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let token = CancellationToken::new();
|
||||
token.cancel(); // Cancel immediately
|
||||
|
||||
// Should return cancellation error
|
||||
let result = compute_file_sha256_with_cancellation(temp_file.path(), &token).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("cancelled"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compute_file_sha256_nonexistent_file() {
|
||||
let token = CancellationToken::new();
|
||||
let nonexistent_path = Path::new("/nonexistent/file.txt");
|
||||
|
||||
let result = compute_file_sha256_with_cancellation(nonexistent_path, &token).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Failed to open file for hashing"));
|
||||
}
|
||||
}
|
||||
@ -48,3 +48,86 @@ pub fn calculate_exponential_backoff_delay(attempt: u32) -> u64 {
|
||||
|
||||
final_delay
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_calculate_exponential_backoff_delay_basic() {
|
||||
let delay1 = calculate_exponential_backoff_delay(1);
|
||||
let delay2 = calculate_exponential_backoff_delay(2);
|
||||
let delay3 = calculate_exponential_backoff_delay(3);
|
||||
|
||||
// First attempt should be around base delay (1000ms) ± jitter
|
||||
assert!(delay1 >= 100 && delay1 <= 2000);
|
||||
|
||||
// Second attempt should be roughly double
|
||||
assert!(delay2 >= 1000 && delay2 <= 4000);
|
||||
|
||||
// Third attempt should be roughly quadruple
|
||||
assert!(delay3 >= 2000 && delay3 <= 6000);
|
||||
|
||||
// Generally increasing pattern
|
||||
assert!(delay1 < delay3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_exponential_backoff_delay_max_cap() {
|
||||
// Very high attempt numbers should be capped at MAX_RESTART_DELAY_MS
|
||||
let high_attempt_delay = calculate_exponential_backoff_delay(100);
|
||||
assert!(high_attempt_delay <= MCP_MAX_RESTART_DELAY_MS);
|
||||
assert!(high_attempt_delay >= 100); // Minimum delay
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_exponential_backoff_delay_minimum() {
|
||||
// Even with jitter, should never go below minimum
|
||||
for attempt in 1..=10 {
|
||||
let delay = calculate_exponential_backoff_delay(attempt);
|
||||
assert!(delay >= 100, "Delay {} for attempt {} is below minimum", delay, attempt);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_exponential_backoff_delay_deterministic() {
|
||||
// Same attempt number should produce same delay (deterministic jitter)
|
||||
let delay1_a = calculate_exponential_backoff_delay(5);
|
||||
let delay1_b = calculate_exponential_backoff_delay(5);
|
||||
assert_eq!(delay1_a, delay1_b);
|
||||
|
||||
let delay2_a = calculate_exponential_backoff_delay(10);
|
||||
let delay2_b = calculate_exponential_backoff_delay(10);
|
||||
assert_eq!(delay2_a, delay2_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_exponential_backoff_delay_progression() {
|
||||
// Test the general progression pattern
|
||||
let mut delays = Vec::new();
|
||||
for attempt in 1..=8 {
|
||||
delays.push(calculate_exponential_backoff_delay(attempt));
|
||||
}
|
||||
|
||||
// Should not exceed maximum
|
||||
for delay in &delays {
|
||||
assert!(*delay <= MCP_MAX_RESTART_DELAY_MS);
|
||||
}
|
||||
|
||||
// Earlier attempts should generally be smaller than later ones
|
||||
// (allowing some variance due to jitter)
|
||||
assert!(delays[0] < delays[6]); // 1st vs 7th attempt
|
||||
assert!(delays[1] < delays[7]); // 2nd vs 8th attempt
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constants() {
|
||||
// Verify our constants are reasonable
|
||||
assert_eq!(MCP_BASE_RESTART_DELAY_MS, 1000);
|
||||
assert_eq!(MCP_MAX_RESTART_DELAY_MS, 30000);
|
||||
assert_eq!(MCP_BACKOFF_MULTIPLIER, 2.0);
|
||||
|
||||
// Max should be greater than base
|
||||
assert!(MCP_MAX_RESTART_DELAY_MS > MCP_BASE_RESTART_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,3 +73,130 @@ pub fn is_memory_pattern(content: &str) -> bool {
|
||||
&& part.contains("MiB")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_uuid() {
|
||||
let uuid_bytes = [
|
||||
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88
|
||||
];
|
||||
|
||||
let uuid_string = parse_uuid(&uuid_bytes);
|
||||
assert_eq!(uuid_string, "12345678-9abc-def0-1122-334455667788");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_uuid_zeros() {
|
||||
let zero_bytes = [0; 16];
|
||||
let uuid_string = parse_uuid(&zero_bytes);
|
||||
assert_eq!(uuid_string, "00000000-0000-0000-0000-000000000000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_uuid_max_values() {
|
||||
let max_bytes = [0xff; 16];
|
||||
let uuid_string = parse_uuid(&max_bytes);
|
||||
assert_eq!(uuid_string, "ffffffff-ffff-ffff-ffff-ffffffffffff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_c_string() {
|
||||
let c_string = [b'H' as i8, b'e' as i8, b'l' as i8, b'l' as i8, b'o' as i8, 0, b'W' as i8];
|
||||
let result = parse_c_string(&c_string);
|
||||
assert_eq!(result, "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_c_string_empty() {
|
||||
let empty_c_string = [0];
|
||||
let result = parse_c_string(&empty_c_string);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_c_string_no_null_terminator() {
|
||||
let no_null = [b'T' as i8, b'e' as i8, b's' as i8, b't' as i8];
|
||||
let result = parse_c_string(&no_null);
|
||||
assert_eq!(result, "Test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_c_string_with_negative_values() {
|
||||
let with_negative = [-1, b'A' as i8, b'B' as i8, 0];
|
||||
let result = parse_c_string(&with_negative);
|
||||
// Should convert negative to unsigned byte
|
||||
assert!(result.len() > 0);
|
||||
assert!(result.contains('A'));
|
||||
assert!(result.contains('B'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_err_to_string() {
|
||||
let error_msg = "Something went wrong";
|
||||
let result = err_to_string(error_msg);
|
||||
assert_eq!(result, "Error: Something went wrong");
|
||||
|
||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
|
||||
let result = err_to_string(io_error);
|
||||
assert!(result.starts_with("Error: "));
|
||||
assert!(result.contains("File not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_memory_pattern_valid() {
|
||||
assert!(is_memory_pattern("8128 MiB, 8128 MiB free"));
|
||||
assert!(is_memory_pattern("1024 MiB, 512 MiB free"));
|
||||
assert!(is_memory_pattern("16384 MiB, 12000 MiB free"));
|
||||
assert!(is_memory_pattern("0 MiB, 0 MiB free"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_memory_pattern_invalid() {
|
||||
assert!(!is_memory_pattern("8128 MB, 8128 MB free")); // Wrong unit
|
||||
assert!(!is_memory_pattern("8128 MiB 8128 MiB free")); // Missing comma
|
||||
assert!(!is_memory_pattern("8128 MiB, 8128 MiB used")); // Wrong second part
|
||||
assert!(!is_memory_pattern("not_a_number MiB, 8128 MiB free")); // Invalid number
|
||||
assert!(!is_memory_pattern("8128 MiB")); // Missing second part
|
||||
assert!(!is_memory_pattern("")); // Empty string
|
||||
assert!(!is_memory_pattern("8128 MiB, free")); // Missing number in second part
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern() {
|
||||
let text = "Loading model... (8128 MiB, 4096 MiB free) completed";
|
||||
let result = find_memory_pattern(text);
|
||||
assert!(result.is_some());
|
||||
let (start_idx, content) = result.unwrap();
|
||||
assert!(start_idx > 0);
|
||||
assert_eq!(content, "8128 MiB, 4096 MiB free");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern_multiple_parentheses() {
|
||||
let text = "Start (not memory) then (1024 MiB, 512 MiB free) and (2048 MiB, 1024 MiB free) end";
|
||||
let result = find_memory_pattern(text);
|
||||
assert!(result.is_some());
|
||||
let (_, content) = result.unwrap();
|
||||
// Should return the LAST valid memory pattern
|
||||
assert_eq!(content, "2048 MiB, 1024 MiB free");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern_no_match() {
|
||||
let text = "No memory info here";
|
||||
assert!(find_memory_pattern(text).is_none());
|
||||
|
||||
let text_with_invalid = "Some text (invalid memory info) here";
|
||||
assert!(find_memory_pattern(text_with_invalid).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_memory_pattern_unclosed_parenthesis() {
|
||||
let text = "Unclosed (8128 MiB, 4096 MiB free";
|
||||
assert!(find_memory_pattern(text).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,131 @@ vi.mock('@jan/extensions-web', () => ({
|
||||
WEB_EXTENSIONS: {}
|
||||
}))
|
||||
|
||||
// Mock @janhq/core EngineManager to prevent initialization issues
|
||||
vi.mock('@janhq/core', () => ({
|
||||
EngineManager: {
|
||||
instance: vi.fn(() => ({
|
||||
engines: new Map()
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock token.js to avoid initialization issues
|
||||
vi.mock('token.js', () => ({
|
||||
models: {}
|
||||
}))
|
||||
|
||||
// Mock ExtensionManager to avoid initialization issues
|
||||
vi.mock('@/lib/extension', () => ({
|
||||
ExtensionManager: {
|
||||
getInstance: vi.fn(() => ({
|
||||
getEngine: vi.fn()
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock dynamic imports for web services
|
||||
vi.mock('../theme/web', () => ({
|
||||
WebThemeService: vi.fn().mockImplementation(() => ({
|
||||
setTheme: vi.fn(),
|
||||
getCurrentWindow: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('../app/web', () => ({
|
||||
WebAppService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../path/web', () => ({
|
||||
WebPathService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../core/web', () => ({
|
||||
WebCoreService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../dialog/web', () => ({
|
||||
WebDialogService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../events/web', () => ({
|
||||
WebEventsService: vi.fn().mockImplementation(() => ({
|
||||
emit: vi.fn(),
|
||||
listen: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('../window/web', () => ({
|
||||
WebWindowService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../deeplink/web', () => ({
|
||||
WebDeepLinkService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../providers/web', () => ({
|
||||
WebProvidersService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
// Mock dynamic imports for Tauri services
|
||||
vi.mock('../theme/tauri', () => ({
|
||||
TauriThemeService: vi.fn().mockImplementation(() => ({
|
||||
setTheme: vi.fn(),
|
||||
getCurrentWindow: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('../window/tauri', () => ({
|
||||
TauriWindowService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../events/tauri', () => ({
|
||||
TauriEventsService: vi.fn().mockImplementation(() => ({
|
||||
emit: vi.fn(),
|
||||
listen: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('../hardware/tauri', () => ({
|
||||
TauriHardwareService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../app/tauri', () => ({
|
||||
TauriAppService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../mcp/tauri', () => ({
|
||||
TauriMCPService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../providers/tauri', () => ({
|
||||
TauriProvidersService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../dialog/tauri', () => ({
|
||||
TauriDialogService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../opener/tauri', () => ({
|
||||
TauriOpenerService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../updater/tauri', () => ({
|
||||
TauriUpdaterService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../path/tauri', () => ({
|
||||
TauriPathService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../core/tauri', () => ({
|
||||
TauriCoreService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('../deeplink/tauri', () => ({
|
||||
TauriDeepLinkService: vi.fn().mockImplementation(() => ({}))
|
||||
}))
|
||||
|
||||
// Mock console to avoid noise in tests
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
@ -40,6 +40,25 @@ describe('Web-Specific Service Tests', () => {
|
||||
|
||||
describe('WebProvidersService', () => {
|
||||
it('should use browser fetch for API calls', async () => {
|
||||
// Mock the dependencies before importing
|
||||
vi.mock('token.js', () => ({
|
||||
models: {}
|
||||
}))
|
||||
vi.mock('@/lib/extension', () => ({
|
||||
ExtensionManager: {
|
||||
getInstance: vi.fn(() => ({
|
||||
getEngine: vi.fn()
|
||||
}))
|
||||
}
|
||||
}))
|
||||
vi.mock('@janhq/core', () => ({
|
||||
EngineManager: {
|
||||
instance: vi.fn(() => ({
|
||||
engines: new Map()
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
const { WebProvidersService } = await import('../providers/web')
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
@ -66,7 +85,7 @@ describe('Web-Specific Service Tests', () => {
|
||||
})
|
||||
)
|
||||
expect(models).toEqual(['gpt-4'])
|
||||
})
|
||||
}, 10000) // Increase timeout to 10 seconds
|
||||
})
|
||||
|
||||
describe('WebAppService', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user